Skip to Content
Course content

156: ES Module Syntax in TypeScript

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.