TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
156: ES Module Syntax in TypeScript
I've been spending the morning cleaning up a pricing engine for a client, and it's a mess of old require() calls and mixed-up module patterns. Since we're moving this project to a modern TypeScript stack, I want to show you how we actually handle ES Module (ESM) syntax. It's the industry standard now, but if you're coming from a Node.js background, the transition can feel a bit finicky.
Starting with Named Exports
I'll start by creating a utility file for our tax calculations. I usually prefer named exports because they make it much easier for the IDE to find things via autocomplete, and it prevents the "what did the original author name this import?" guessing game.
// pricing-utils.ts
export const TAX_RATE = 0.07;
export function calculateTotal(price: number): number {
return price + (price * TAX_RATE);
}
export function applyDiscount(price: number, discount: number): number {
return price - discount;
}
Now, I'll try to use these in my main entry point. I'll use the curly brace syntax to pick exactly what I need.
// main.ts
import { calculateTotal, TAX_RATE } from './pricing-utils';
const subtotal = 100;
console.log(`Tax rate is ${TAX_RATE}`);
console.log(`Total: ${calculateTotal(subtotal)}`);
This works perfectly. TypeScript knows exactly which functions I'm grabbing. But wait—what if I want to import everything from that file as a single object? I don't want to list ten different functions in my import statement.
Grouping with Namespace Imports
I'll try the * as syntax. This is great when a utility file gets large and I want to keep the namespace clean so I don't have naming collisions with other parts of my app.
// main.ts
import * as Pricing from './pricing-utils';
const subtotal = 100;
console.log(`Total: ${Pricing.calculateTotal(subtotal)}`);
Notice how Pricing.calculateTotal is now clearly grouped. I like this approach for "helper" libraries because it tells anyone reading the code, "This function belongs to the pricing logic, not the general UI logic."
The Default Export Dilemma
Sometimes, a file is designed to do exactly one thing—like export a single class or a main configuration object. In those cases, I'll use a default export. Let's try that by creating a PricingEngine class.
// PricingEngine.ts
export default class PricingEngine {
calculate(items: number[]) {
return items.reduce((acc, curr) => acc + curr, 0);
}
}
Now, watch what happens when I import this. I don't use curly braces anymore.
// main.ts
import PricingEngine from './PricingEngine';
const engine = new PricingEngine();
Here is the "gotcha" I've run into a thousand times: when you use a default export, you can actually name it whatever you want during the import. I could have written import SuperEngine from './PricingEngine' and it would still work. That's why I generally avoid default exports in large projects—it makes searching the codebase for usage much harder.
Handling the Legacy CommonJS Clash
Here is where things usually break. I tried to import a legacy Node package (like lodash or an old internal library) using the ES syntax, and TypeScript threw a fit, complaining that the module didn't have a default export.
If I try import lodash from 'lodash'; and it fails, it's because lodash uses module.exports = ... (CommonJS) rather than export default (ESM). To fix this without writing ugly import * as lodash from 'lodash' calls, I have to tweak the tsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
By turning on esModuleInterop, TypeScript creates a little helper function during compilation that allows us to treat CommonJS modules as if they had a default export. It's a lifesaver for compatibility. I always enable this by default in my projects to avoid the headache.
📋 Practical Task
Refactoring the Inventory Manager to ESM
You have a legacy project with two files using module.exports and require(). Your task is to refactor them to use modern TypeScript ES Module syntax.
File 1: inventory-types.ts
Convert this from CommonJS to Named Exports:
const StockStatus = {
IN_STOCK: 'in_stock',
OUT_OF_STOCK: 'out_of_stock',
BACKORDERED: 'backordered'
};
module.exports = { StockStatus };
File 2: InventoryManager.ts
Convert this class to a Default Export:
class InventoryManager {
updateStock(id: string, amount: number) {
console.log(`Updating ${id} to ${amount}`);
}
}
module.exports = InventoryManager;
File 3: app.ts
Rewrite the imports to use the new ESM syntax. You must import StockStatus as a named import and InventoryManager as a default import.
const { StockStatus } = require('./inventory-types');
const InventoryManager = require('./InventoryManager');
const manager = new InventoryManager();
console.log(StockStatus.IN_STOCK);
There are no comments for now.