ZetCode

TypeScript Modules

last modified October 15, 2026

A module is a file that exports values, functions, classes, or types for use in other files, and imports what it needs from them. TypeScript uses the standard ECMAScript module syntax with import and export. A file that contains at least one import or export is a module: its top-level declarations are private to the file unless they are exported. The compiler checks every import: a misspelled name or a wrong type is reported at the place where it is used.

The examples assume the project setup from the setup tutorial, with "module": "nodenext" in tsconfig.json and "type": "module" in package.json. With this setup, relative imports are written with the .js extension of the compiled file, even though the source file is a .ts file. The files of each example are in the same directory.

Basic Module Syntax

The export keyword in front of a declaration makes it available to other modules. An import statement with curly braces brings named exports into a file.

mathUtils.ts
export function add(a: number, b: number): number {
    return a + b;
}

export function subtract(a: number, b: number): number {
    return a - b;
}

function round(value: number): number {
    return Math.round(value * 100) / 100;
}

export const PI = round(Math.PI);
main.ts
import { add, subtract, PI } from "./mathUtils.js";

console.log(add(10, 5));
console.log(subtract(10, 5));
console.log(PI);

The mathUtils.ts module exports two functions and a constant. The round helper is not exported, so it stays private to the module; importing it in main.ts would be an error. The imported functions keep their types, so add("10", 5) in main.ts would not compile.

$ npx tsx main.ts
15
5
3.14

Exporting Types and Classes

Interfaces, type aliases, and classes are exported in the same way. An export list at the end of a file is an alternative to marking each declaration.

shapes.ts
interface Shape {
    area(): number;
}

type Unit = "cm" | "m";

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}

    area(): number {
        return this.width * this.height;
    }
}

function describe(shape: Shape, unit: Unit): string {
    return `area: ${shape.area()} ${unit}²`;
}

export { Rectangle, describe };
export type { Shape, Unit };
main.ts
import { Rectangle, describe } from "./shapes.js";
import type { Shape, Unit } from "./shapes.js";

const shapes: Shape[] = [new Rectangle(3, 4), { area: () => 10 }];
const unit: Unit = "m";

for (const shape of shapes) {
    console.log(describe(shape, unit));
}

The export list at the end of shapes.ts exports the class and the function. The export type and import type forms are used for declarations that exist only as types: the compiler removes them completely from the JavaScript output. In main.ts, the Shape interface is used to type an array that contains both a class instance and a plain object.

$ npx tsx main.ts
area: 12 m²
area: 10 m²

Default Exports

A module can have one default export, marked with export default. It is imported without curly braces, under any name.

logger.ts
export default function log(message: string): void {
    console.log(`[LOG] ${message}`);
}

export const LEVELS = ["debug", "info", "error"] as const;
main.ts
import log, { LEVELS } from "./logger.js";
import writeLog from "./logger.js";

log("This is a log message.");
writeLog(`Available levels: ${LEVELS.join(", ")}`);

The default export is imported once as log and once as writeLog; both names refer to the same function. A default import can be combined with named imports in one statement. Because the name is chosen by the importing file, named exports are often preferred: they keep names consistent across a code base and work better with editor auto-imports.

$ npx tsx main.ts
[LOG] This is a log message.
[LOG] Available levels: debug, info, error

Renaming Imports and Exports

The as keyword renames an import or an export, for example to avoid a name conflict.

celsius.ts
export function convert(value: number): number {
    return (value * 9) / 5 + 32;
}
fahrenheit.ts
function toCelsius(value: number): number {
    return ((value - 32) * 5) / 9;
}

export { toCelsius as convert };
main.ts
import { convert as celsiusToFahrenheit } from "./celsius.js";
import { convert as fahrenheitToCelsius } from "./fahrenheit.js";

console.log(celsiusToFahrenheit(100));
console.log(fahrenheitToCelsius(212));

Both modules export a function named convert; the second one is renamed already when it is exported. In main.ts, both imports are given new, descriptive names, so they can be used in the same file.

$ npx tsx main.ts
212
100

Namespace Imports

The import * as form imports all exports of a module as properties of one object.

mathUtils.ts
export function add(a: number, b: number): number {
    return a + b;
}

export function multiply(a: number, b: number): number {
    return a * b;
}

export const E = 2.718;
main.ts
import * as MathUtils from "./mathUtils.js";

console.log(MathUtils.add(10, 5));
console.log(MathUtils.multiply(10, 5));
console.log(MathUtils.E);

The MathUtils object groups the exports and makes their origin visible at every use. It is typed: the editor offers its members and MathUtils.divide would be an error.

$ npx tsx main.ts
15
50
2.718

Re-exporting Modules

A module can re-export what other modules export. An index.ts file that collects the public API of a directory is often called a barrel.

strings.ts
export function capitalize(text: string): string {
    return text.charAt(0).toUpperCase() + text.slice(1);
}
numbers.ts
export function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
}

export function isEven(value: number): boolean {
    return value % 2 === 0;
}
index.ts
export { capitalize } from "./strings.js";
export * from "./numbers.js";
main.ts
import { capitalize, clamp, isEven } from "./index.js";

console.log(capitalize("typescript"));
console.log(clamp(150, 0, 100));
console.log(isEven(42));

The index.ts file re-exports one function by name and everything from numbers.ts with export *. The main module imports from a single place and does not depend on how the helpers are split into files. Barrels are convenient for libraries, but in large applications they can slow down builds and tests, because importing one function loads the whole barrel.

$ npx tsx main.ts
Typescript
100
true

Dynamic Imports

The import() expression loads a module at runtime and returns a promise of the module object.

report.ts
export function generateReport(items: string[]): string {
    return items.map((item, i) => `${i + 1}. ${item}`).join("\n");
}
main.ts
async function printReport(items: string[]): Promise<void> {
    console.log("loading report module...");
    const { generateReport } = await import("./report.js");
    console.log(generateReport(items));
}

printReport(["Sales", "Costs", "Profit"]);

The module is loaded only when printReport runs. In browser applications, bundlers turn dynamic imports into separate files that are downloaded on demand, which reduces the initial loading time. The result is fully typed: TypeScript knows that the module has a generateReport function taking a string[].

$ npx tsx main.ts
loading report module...
1. Sales
2. Costs
3. Profit

Module Scope

Each module has its own scope. Declarations with the same name in different modules do not conflict.

counterA.ts
let count = 0;

export function nextA(): number {
    count += 1;
    return count;
}
counterB.ts
let count = 100;

export function nextB(): number {
    count += 10;
    return count;
}
main.ts
import { nextA } from "./counterA.js";
import { nextB } from "./counterB.js";

console.log(nextA(), nextA());
console.log(nextB(), nextB());

Both modules have a private count variable, and each function keeps changing its own. A module is evaluated only once, no matter how many files import it, so its top-level variables keep their state for the whole program. A file without import or export is a script instead: its declarations are global, which is why an empty export {} is sometimes added to turn a file into a module.

$ npx tsx main.ts
1 2
110 120

Modules and Namespaces

Before ECMAScript modules existed, TypeScript code was organized with namespaces. A namespace can still be exported from a module.

geometry.ts
export namespace Geometry {
    export function circleArea(radius: number): number {
        return Math.PI * radius ** 2;
    }

    export const UNIT = "cm";
}
main.ts
import { Geometry } from "./geometry.js";

console.log(`${Geometry.circleArea(2).toFixed(2)} ${Geometry.UNIT}²`);

Inside a module, a namespace only adds another level of nesting; the module already provides a separate scope. Modern code uses modules, and namespaces are mainly found in older code and in declaration files. See the namespaces tutorial for details.

$ npx tsx main.ts
12.57 cm²

Best Practices

Source

TypeScript Handbook: Modules

This tutorial covered modules in TypeScript. Each file is its own scope, and import and export make the dependencies between files explicit and type-checked.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.