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;
}
▶ Try it Yourself
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:

  1. ./utils.ts
  2. ./utils.tsx
  3. ./utils.d.ts
  4. The types field in ./utils/package.json
  5. ./utils/index.ts
  6. ./utils/index.d.ts

(3) Searching in node_modules

TEXT 📖 Display only
import _ from "mylib"

Search Order:

  1. ./node_modules/mylib.ts (Does not exist)
  2. ./node_modules/mylib/package.jsontypes/typings field
  3. ./node_modules/mylib/index.d.ts
  4. ./node_modules/@types/mylib/index.d.ts
  5. 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}`; }
}
▶ Try it Yourself
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;
}
▶ Try it Yourself
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

📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Create three module files—math.ts (exports add/subtract), string-utils.ts (exports capitalize/reverse), and index.ts (re-exports the bucket file). Import and use them in main.ts.
  2. Advanced Exercise (Difficulty ⭐⭐): Configure path mappings for an existing project—map @utils to src/utils, and @models to src/models. Use import type to import types, and use plain import to import values.
  3. Challenge (Difficulty: ⭐⭐⭐): Write a declaration file that allows the CommonJS package legacy-sdk to be imported into TypeScript in ES Module style—import LegacySDK from "legacy-sdk". Consider both cases where esModuleInterop is enabled and disabled.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏