TypeScript: The TypeScript Module System
Last updated: 2026-08-26
Modules are the basic units for organizing code—TypeScript supports ES modules and CommonJS modules, and adds the ability to export types on top of them.
1. ES Modules Basics
(1) Naming Export
TYPESCRIPT
// utils.ts —— Name Export
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const PI = 3.14159;
TYPESCRIPT
// main.ts —— Import by Name
import { add, multiply, PI } from "./utils";
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12
console.log(PI); // 3.14159
(2) Default Export
TYPESCRIPT
// logger.ts —— Default Export
export default class Logger {
constructor(private prefix: string) {}
log(message: string): void {
console.log(`[${this.prefix}] ${message}`);
}
}
TYPESCRIPT
// main.ts —— Default Import(No curly braces needed,Custom Name)
import Logger from "./logger";
// That's fine, too.:import MyLogger from "./logger";
let logger = new Logger("APP");
logger.log("App Launch"); // [APP] App Launch
(3) Using both default exports and named exports
TYPESCRIPT
// api.ts
export default class ApiClient {
constructor(private baseUrl: string) {}
async get(path: string): Promise<any> {
// ...
}
}
export enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
TYPESCRIPT
// main.ts
import ApiClient, { HttpMethod } from "./api";
let client = new ApiClient("https://api.example.com");
let method: HttpMethod = HttpMethod.GET;
▶ Example: A Modular Calculator
TYPESCRIPT
// calculator/operations.ts
export function add(a: number, b: number): number { return a + b; }
export function subtract(a: number, b: number): number { return a - b; }
export function multiply(a: number, b: number): number { return a * b; }
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("The divisor cannot be zero.");
return a / b;
}
TYPESCRIPT
// calculator/index.ts
export { add, subtract, multiply, divide } from "./operations";
export type { Operation } from "./types";
// Default Export——Calculator Category
import * as ops from "./operations";
export default class Calculator {
compute(op: string, a: number, b: number): number {
switch (op) {
case "+": return ops.add(a, b);
case "-": return ops.subtract(a, b);
case "*": return ops.multiply(a, b);
case "/": return ops.divide(a, b);
default: throw new Error(`Unknown Operation:${op}`);
}
}
}
2. Type Export
TypeScript allows you to export types individually—a capability that the JavaScript module system lacks:
(1) The type modifier
TYPESCRIPT
// types.ts
export interface User {
id: number;
name: string;
email: string;
}
export type UserId = number;
export type UserRole = "admin" | "editor" | "viewer";
// Use export type to explicitly mark"Export Types Only"
export type { User as UserType };
(2) Distinguish Between Types and Values When Importing
TYPESCRIPT
// main.ts
import { type User, type UserRole, createUser } from "./types";
// ↑ type Modifier Notation"Import Types Only"——It will be erased after compilation.
// Equivalent Old Syntax
// import { User, UserRole } from "./types"; // May result in runtime imports
// Recommended New Syntax——Clearly Distinguish Between Type Import and Value Import
import type { User, UserRole } from "./types";
import { createUser } from "./types";
💡 Why make this distinction?
import type Imported types are completely erased after compilation—they do not result in runtime calls to require or import. This is crucial for scenarios that only use types (such as type annotations and interfaces), as it prevents unnecessary module loading.
(3) Inline type imports
TYPESCRIPT
// Mixed Import——Values and Types
import { createUser, type User, type UserRole } from "./types";
// createUser is the value——Requirements for runtime
// User and UserRole is a type——Compile-Time Erasure
3. Re-exporting and Bucket Files
(1) Re-export
TYPESCRIPT
// Re-exporting members of one module from another module
export { User, UserId } from "./user-types";
export { Product, ProductId } from "./product-types";
export { Order, OrderId } from "./order-types";
(2) Barrel File
index.ts As the entry point for the directory, re-export all public APIs:
TYPESCRIPT
// models/index.ts —— Bucket files
export { User, UserId } from "./user";
export { Product, ProductId } from "./product";
export { Order, OrderId } from "./order";
export type { CreateUser, UpdateUser } from "./user";
export type { CreateProduct, UpdateProduct } from "./product";
TYPESCRIPT
// Import directly from the directory when using it
import { User, Product, type CreateUser } from "./models";
// without needing to know which specific file it is in
💡 Pros: Simplifies import paths, hides the internal file structure, and controls the public API. Cons: May import unnecessary modules (tree-shaking may not fully optimize the code).
4. Module Parsing Strategies
TypeScript needs to know how to resolve import "./utils" to an actual file—this is determined by the module resolution strategy.
(1) Two Parsing Strategies
| Strategy | Purpose | Description |
|---|---|---|
classic |
Legacy version compatibility | Search for .ts first, then .d.ts |
node (Recommended) |
Modern TS Project | Simulates Node.js Parsing Logic |
(2) Lookup Order for Node Parsing Strategies
TEXT
📖 Display only
import { X } from "./utils"
Search Order:
./utils.ts./utils.tsx./utils.d.ts- The
typesfield in./utils/package.json ./utils/index.ts./utils/index.d.ts
(3) Searching in node_modules
TEXT
📖 Display only
import _ from "mylib"
Search Order:
./node_modules/mylib.ts(Does not exist)./node_modules/mylib/package.json→types/typingsfield./node_modules/mylib/index.d.ts./node_modules/@types/mylib/index.d.ts- Search up
../node_modules/→../../node_modules/...
5. path mapping(Path Mapping)
Use path aliases in large projects to avoid long relative paths:
(1) tsconfig.json Configuration
JSON
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"],
"@components/*": ["src/components/*"]
}
}
}
(2) Using Path Aliases
TYPESCRIPT
// No aliases——Relative paths are prone to errors
import { User } from "../../../models/user";
import { formatDate } from "../../utils/date";
// Has an alias——Clear and concise
import { User } from "@models/user";
import { formatDate } from "@utils/date";
⚠️ Note: Path aliases are only mappings at compile time—the runtime (Node.js/browser) does not recognize paths like
@models. You need to use a build tool (such as Webpack’s resolve.alias, Vite’s resolve.alias, or tsc-alias) to perform runtime path substitution.
6. CommonJS Interoperability
(1) CommonJS Modules
TYPESCRIPT
// Use CommonJS style export
// math.cjs
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
module.exports = { add, multiply };
(2) Importing CommonJS in TS
TYPESCRIPT
// esModuleInterop: false(Default)
import * as math from "./math.cjs";
math.add(1, 2);
// esModuleInterop: true(Recommendations)
import math from "./math.cjs"; // ✅ A More Natural Way to Introduce It
math.add(1, 2);
(3) Allow CommonJS exports from TS
TYPESCRIPT
// Use export = syntax to export CommonJS Style
class Calculator {
add(a: number, b: number): number { return a + b; }
}
export = Calculator;
// Use the following when importing: import = require
import Calculator = require("./calculator");
let calc = new Calculator();
💡 Recommendation: Use ES modules (
import/export) for all new projects, and enable esModuleInterop: true to maintain compatibility with legacy CommonJS packages. Use export = and import = only when strict compatibility with CommonJS is required.
▶ Example: Named vs Default Exports Side by Side
TYPESCRIPT
// config.ts — named and default exports together
export const APP_NAME = "MyApp";
export const VERSION = "1.0.0";
export default class AppConfig {
constructor(public port: number = 3000) {}
toString(): string { return `${APP_NAME} v${VERSION} on :${this.port}`; }
}
TYPESCRIPT
// main.ts — importing both styles
import AppConfig, { APP_NAME, VERSION } from "./config";
let config = new AppConfig(8080);
console.log(config.toString()); // MyApp v1.0.0 on :8080
console.log(APP_NAME); // MyApp
Output:
TEXT
📖 Display only
MyApp v1.0.0 on :8080
MyApp
▶ Example: import type for Type-Only Imports
TYPESCRIPT
// shapes.ts
export interface Circle { kind: "circle"; radius: number; }
export interface Square { kind: "square"; side: number; }
export type Shape = Circle | Square;
export function area(shape: Shape): number {
return shape.kind === "circle"
? Math.PI * shape.radius ** 2
: shape.side ** 2;
}
TYPESCRIPT
// main.ts — import type for types, regular import for values
import type { Circle, Square, Shape } from "./shapes";
import { area } from "./shapes";
let c: Circle = { kind: "circle", radius: 5 };
let s: Shape = { kind: "square", side: 4 };
console.log(area(c)); // 78.5398...
console.log(area(s)); // 16
Output:
TEXT
📖 Display only
78.53981633974483
16
❓ FAQ
Q What is the difference between
import type and a regular import?A
import type Types imported this way are completely erased after compilation and do not result in runtime module loading. A regular import statement imports both values and types, and results in a require() call after compilation. When using only types (such as interfaces or type aliases), be sure to use import type to avoid unnecessary runtime imports.Q Should bucket files (index.ts) be used?
A They are recommended for libraries and public APIs—to simplify imports and control exported interfaces. For internal applications, it depends on the situation—they aren’t necessary for small projects, but offer organizational benefits for large projects. The main drawback is that they may affect tree-shaking, but modern bundlers handle this well.
Q How do path aliases take effect at runtime?
A The JS code generated by TypeScript still uses alias paths (such as
@models/user), which are not recognized at runtime. Path replacement must be handled by a build tool—Webpack uses resolve.alias, Vite uses resolve.alias, and pure tsc compilation requires post-processing by tools like tsc-alias.Q Which should I use, ES Modules or CommonJS?
A For new projects, use ES Modules exclusively (
import/export). CommonJS is Node.js’s legacy module system and is being phased out. The esModuleInterop option in TypeScript allows you to use CommonJS packages seamlessly. Browsers and Deno only support ES Modules.📖 Summary
- ES modules use
export/importas organization codes; curly braces are used for named exports, but not for default exports. import typeImport types only—erase after compilation; no runtime module loading occurs- The
index.tsbucket file re-exports the public APIs in the directory to simplify the import path - Module Parsing Strategy
node(Recommended) Simulates Node.js search logic - Path mapping (
baseUrl+paths) replaces long relative paths with aliases; requires a build tool to support this esModuleInterop: trueEnabling Seamless Interoperability Between ES Modules and CommonJS
📝 Exercises
- Basic Exercise (Difficulty ⭐): Create three module files—
math.ts(exports add/subtract),string-utils.ts(exports capitalize/reverse), andindex.ts(re-exports the bucket file). Import and use them inmain.ts. - Advanced Exercise (Difficulty ⭐⭐): Configure path mappings for an existing project—map
@utilstosrc/utils, and@modelstosrc/models. Useimport typeto import types, and use plainimportto import values. - Challenge (Difficulty: ⭐⭐⭐): Write a declaration file that allows the CommonJS package
legacy-sdkto be imported into TypeScript in ES Module style—import LegacySDK from "legacy-sdk". Consider both cases whereesModuleInteropis enabled and disabled.