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.
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);
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.
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 };
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.
export default function log(message: string): void {
console.log(`[LOG] ${message}`);
}
export const LEVELS = ["debug", "info", "error"] as const;
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.
export function convert(value: number): number {
return (value * 9) / 5 + 32;
}
function toCelsius(value: number): number {
return ((value - 32) * 5) / 9;
}
export { toCelsius as convert };
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.
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;
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.
export function capitalize(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1);
}
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;
}
export { capitalize } from "./strings.js";
export * from "./numbers.js";
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.
export function generateReport(items: string[]): string {
return items.map((item, i) => `${i + 1}. ${item}`).join("\n");
}
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.
let count = 0;
export function nextA(): number {
count += 1;
return count;
}
let count = 100;
export function nextB(): number {
count += 10;
return count;
}
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.
export namespace Geometry {
export function circleArea(radius: number): number {
return Math.PI * radius ** 2;
}
export const UNIT = "cm";
}
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
- Use ES Modules: Organize code with
importandexportrather than namespaces or global scripts. - Prefer Named Exports: They keep names consistent and work well with editor auto-imports and refactoring.
- Mark Type-Only Imports: Use
import typefor types; theverbatimModuleSyntaxoption makes the compiler enforce it. - Keep Modules Focused: Give each module one responsibility and export only its public API.
- Use Barrels Carefully: An
index.tssimplifies imports for library users, but large barrels slow down builds. - Load on Demand: Use dynamic
import()for code that is needed only in some situations.
Source
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
List all TypeScript tutorials.