首页 / TypeScript 入门教程 / ES 模块入门

TypeScript 入门教程

ES 模块入门

本教程共 80 篇 · 第 54 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程ES模块importexport动态导入

本节目标:掌握 TypeScript 中的 ES 模块写法——命名导出、默认导出、重导出、动态 import(),以及 TypeScript 判断文件是否为模块的规则。

在 JavaScript 的演进历史中,模块化方案经历过多次迭代。从早期的 IIFE 到 CommonJS,再到 AMD——最终,社区和 TC39 委员会在 ES2015 里统一了标准:ES Module。TypeScript 从 1.5 开始就支持这套语法,到了 7.0,ES Module 更是成了唯一的推荐方案。

一个文件是不是模块?

先看一个经常让新手困惑的问题:export {} 到底是干嘛的?

TypeScript 按照和 JavaScript 相同的规则判断文件身份:如果文件里包含顶层 importexport 语句,它就是一个模块(module);否则,它是一个脚本(script)

模块和脚本的区别很大——模块有自己的作用域,里面的变量不会跑到全局。脚本没有这个隔离,所有顶层声明都暴露到全局空间。

// math.ts——这是一个模块,因为有 export
export const PI = 3.14159;

// utils.ts——这不是模块,它是一个脚本
const VERSION = "1.0.0";

如果某个文件没有任何 importexport,但你又希望它是个模块,加一行:

export {};

这行代码本身不导出任何东西,但它让文件变成了模块。TS 7.0 中 types 的默认值改为 [] 后,这个技巧用得更频繁——因为全局类型不再自动加载了。

命名导出(Named Export)

最常见的导出方式,一个文件可以导出任意多个命名成员:

// math.ts
export const PI = 3.14159;
export const E = 2.71828;

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

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

导入时使用解构语法,名字必须一致:

// app.ts
import { PI, add, Calculator } from "./math.js";

console.log(PI);                // 3.14159
console.log(add(1, 2));        // 3
const calc = new Calculator();
console.log(calc.multiply(2, 3)); // 6

注意导入路径末尾的 .js——TypeScript 的 ES 模块导入习惯带上完整扩展名。这是遵循 Node.js 和浏览器原生 ESM 规范的写法,TypeScript 编译器会自动将 .js 对应到 .ts 源文件。

如果想给导入的成员换个名字,用 as

import { PI as π, add as sum } from "./math.js";
console.log(π);             // 3.14159
console.log(sum(5, 3));    // 8

默认导出(Default Export)

一个模块只能有一个默认导出:

// greeter.ts
export default function greet(name: string): string {
  return `Hello, ${name}!`;
}

导入时不用解构语法,直接取一个名字:

import greet from "./greeter.js";
console.log(greet("TypeScript")); // Hello, TypeScript!

默认导出的东西可以是任何值:函数、类、对象、甚至是原始值。

Note

默认导出和命名导出可以共存。但实际项目里混用会让引用方困惑——是直接用默认名还是解构名?团队内部最好统一风格。

// shapes.ts
export default class Circle {
  constructor(public radius: number) {}
}
export function squareArea(side: number): number {
  return side * side;
}

// app.ts
import Circle, { squareArea } from "./shapes.js";
const c = new Circle(5);
console.log(squareArea(4)); // 16

命名空间导入

把整个模块的所有导出打包成一个对象,用 * as

import * as MathLib from "./math.js";

console.log(MathLib.PI);
console.log(MathLib.add(3, 4));
const c = new MathLib.Calculator();

这种写法适合工具函数库——所有导出一目了然,而且不会和当前作用域的变量名冲突。

副作用导入

有时你只想执行一个模块的代码,不需要引用它的导出:

import "./setup.js";

这行代码会让 setup.js 里的所有顶层代码执行一遍。常见用途是注册全局 polyfill、运行初始化脚本。TypeScript 不会移除这类导入——即使你没有使用任何导出,它依然保留在编译输出中。

重导出(Re-export)

重导出让你把一个模块的导出”转发”到另一个模块,这在构建库的 index.ts 入口文件时特别有用:

// utils/index.ts
export { sleep, debounce } from "./async.js";
export { formatDate, parseDate } from "./date.js";

上面的代码等价于从 ./async.js 导入再导出,但只写了一行。还可以在重导出时改名:

export { sleep as wait } from "./async.js";

全部重导出:

export * from "./async.js";

这一行会把 ./async.js 里所有命名导出都重新暴露出去。但默认导出不会包含在内——export * 只转发命名导出。

如果想转发一个模块的默认导出,需要写:

export { default } from "./core.js";
// 或者改名后再导出:
export { default as CoreModule } from "./core.js";

动态 import()

前面讲的都是静态导入——模块路径在编译时就确定了。动态 import() 让模块加载发生在运行时:

async function loadChart() {
  const { renderChart } = await import("./chart.js");
  renderChart(data);
}

import() 返回一个 Promise,解析后拿到模块的命名空间对象。这意味着你可以按需加载——只在用户触发某个操作时才引入对应的模块。

动态导入最大的好处是代码分割(code splitting)。打包工具(如 esbuild、Rollup)看到 import() 会自动把这个模块拆成一个独立的 chunk,浏览器按需下载。

Tip

TypeScript 7.0 本身不处理代码分割——那是打包工具的事。但 TypeScript 的类型系统完全理解动态导入,import() 的返回类型会被正确推断为模块类型。

TypeScript 特有的模块语法

还有两种 ES 模块的 TypeScript 扩展值得一提:

import type 专门用来导入类型,编译后不产生任何 JavaScript 代码。下一章会详细讲。

import x = require("...") 是兼容 CommonJS 的语法:

import fs = require("fs");
const content = fs.readFileSync("config.json", "utf8");

这种写法在 TS 7.0 仍然可用,但官方建议新代码使用 ES Module 语法配合 module: "nodenext""node16"。如果你在维护用 CommonJS 的老项目,这个语法比 const fs = require("fs") 好的地方是类型推断更准确。

小结

ES 模块是 TypeScript 组织代码的基石。记住这几条核心规则:顶层 import/export 决定文件是不是模块;命名导出可以多个、默认导出只能一个;重导出是库入口文件的常用技巧;动态 import() 实现按需加载。这些规则在 TS 7.0 和未来版本中都是稳定不变的。