前置知识: JavaScript

TypeScript 类型声明与模块解析

22 min中级

TypeScript 声明文件(.d.ts)结构、模块解析策略(Node/NodeNext/Bundler)、ESM/CJS 互操作、路径映射与包导出的全面工程指南

前置知识

学习目标

  • 掌握「1. 学习导论」的核心机制、典型用法与常见陷阱
  • 掌握「2. 历史动机与技术演进」的核心机制、典型用法与常见陷阱
  • 掌握「3. 声明文件基础」的核心机制、典型用法与常见陷阱
  • 掌握「4. 全局声明与命名空间」的核心机制、典型用法与常见陷阱
  • 掌握「5. 模块声明」的核心机制、典型用法与常见陷阱

阅读提示:正文以代码和白话为主,不出现类型论公式。进阶文档中若出现 Γ ⊢ e : τ 这类记号,第一遍可完全跳过(完整规则见 001-HowToReadThisCourse)。

1. 学习导论

1.1 为什么必须理解模块解析

TypeScript 工程实践中,“找不到模块”、“找不到声明文件”、“类型推断为 any”这三类错误占了所有类型相关错误的相当大比例。它们的根源通常不在类型本身,而在模块解析策略的配置与声明文件的组织方式上。

理解模块解析,意味着能够回答以下问题:

  1. 当 TypeScript 看到 import { x } from 'foo' 时,它如何确定 foo 对应的文件路径?
  2. 当 npm 包没有内置类型时,TypeScript 如何从 @types/foo 找到声明?
  3. 当一个包同时发布 ESM 与 CJS 双格式时,TypeScript 如何选择正确的入口?
  4. 当 monorepo 中有多个包相互引用时,如何配置 paths 与 references 才能既保证类型检查又保证构建正确?

1.2 Bloom 认知层次对照

Bloom 层次对应能力本文对应章节
remember记住 .d.ts 结构与解析策略名称第 3、8 节
understand理解四种解析策略的差异第 8 节
apply配置 tsconfig.json 与 exports第 9、10、15 节
analyze分析 ESM/CJS 互操作第 11 节
evaluate评估包导出符合性第 14 节
create设计 monorepo 模块策略第 16 节

2. 历史动机与技术演进

2.1 JavaScript 模块系统演进

JavaScript 的模块系统经历了漫长的演进,每次演进都直接影响 TypeScript 的模块解析策略:

年份规范关键特性影响
1995无模块系统全局变量命名空间污染
2009CommonJS (CJS)require/module.exports,同步加载Node.js 服务器端
2011AMDdefine/require,异步加载浏览器 RequireJS
2013UMD兼容 CJS/AMD/global跨环境库
2015ES Modules (ESM)import/export,静态分析浏览器原生
2018Node.js ESM.mjs/type: moduleNode.js 双格式
2019Node.js 12+exports 字段子路径导出
2020Node.js 14+Conditional exports条件导出
2023TypeScript 5.0moduleResolution: bundler现代打包器

2.2 TypeScript 模块解析演进

TypeScript 版本关键模块特性
1.0 (2014)仅支持 classic 与 node 解析策略
1.5 (2015)支持 ES6 模块语法
2.0 (2016)引入 paths 路径映射
3.0 (2018)引入 --build 与 project references
3.8 (2020)支持 import type
4.7 (2022)引入 moduleResolution: node16/nodenext(exports 字段解析随之落地)
5.0 (2023)稳定 moduleResolution: bundler
5.8 (2025)--module node18、--erasableSyntaxOnly 与 require(esm) 渐进支持

2.3 关键设计者

  • Daniel Rosenwasser:TypeScript 项目主管,主导 moduleResolution 策略演进。
  • Andrew Branch:TypeScript 团队成员,模块解析与声明文件核心贡献者,撰写了大量官方模块解析文档。
  • Guy Bedford:Node.js 模块系统核心贡献者,主导 exports 字段标准化。
  • Wesley Wiggs:DefinitelyTyped 维护者,维护 @types 生态。

3. 声明文件基础

3.1 什么是 .d.ts 文件

声明文件(declaration file)以 .d.ts 为扩展名,包含 TypeScript 类型声明但不包含可执行代码。它描述 JavaScript 代码的类型形状,让 TypeScript 能在不接触实现的情况下提供类型检查与智能提示。

典型使用场景:

  1. 为纯 JavaScript 库提供类型:例如 jQuery、lodash 早期版本。
  2. 为编译产物提供类型:TypeScript 项目编译后生成 .js + .d.ts,使用者只需 .d.ts。
  3. 为运行时注入的全局变量提供类型:例如 window.__APP_CONFIG__。
  4. 为环境 API 提供类型:例如 DOM、Node.js 内置模块。

3.2 声明文件的基本结构

// utils.d.ts - 最简声明文件

// 声明常量
declare const VERSION: string;

// 声明函数
declare function add(a: number, b: number): number;

// 声明类
declare class Calculator {
  constructor(precision?: number);
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

// 声明枚举
declare enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

// 声明命名空间
declare namespace Utils {
  function format(date: Date): string;
  const locale: string;
}

// 声明模块(用于无类型的 npm 包)
declare module 'untyped-lib' {
  export function doSomething(value: string): number;
  export const version: string;
}

3.3 声明文件的三种作用域

TypeScript 中的声明文件按作用域分为三类:

  1. 全局声明:直接在文件顶层声明,自动合并到全局作用域。
  2. 模块声明:包含 import/export 的文件被视为模块,声明只在该模块内有效。
  3. 环境声明:使用 declare 关键字声明已存在的全局变量或模块。
// globals.d.ts - 全局声明
declare const __DEV__: boolean;
declare const __APP_VERSION__: string;

// 必须有 export {} 才能使用 declare global
export {};

declare global {
  interface Window {
    myApp: {
      version: string;
      init(): void;
    };
  }
}

3.4 生成声明文件

TypeScript 编译器可自动生成声明文件:

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,           // 生成 .d.ts
    "declarationMap": true,        // 生成 .d.ts.map(用于跳转源码)
    "emitDeclarationOnly": false,  // 仅生成声明(不生成 .js),常用于库的类型包
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

生成的 .d.ts 文件结构示例:

// 源文件 src/calculator.ts
export class Calculator {
  constructor(private precision: number) {}
  add(a: number, b: number): number {
    return a + b;
  }
}

// 自动生成的 dist/calculator.d.ts
export declare class Calculator {
  private precision;
  constructor(precision: number);
  add(a: number, b: number): number;
}

注意生成的声明文件会移除实现细节(函数体、默认值),仅保留类型签名。


4. 全局声明与命名空间

4.1 declare global

declare global 用于在模块文件中扩展全局作用域。这是现代 TypeScript 推荐的扩展全局类型的方式:

// types/globals.d.ts
export {};  // 关键:必须有 export 才是模块

declare global {
  // 扩展 Window 接口
  interface Window {
    __APP_VERSION__: string;
    __DEV__: boolean;
    analytics?: {
      track(event: string, props?: Record<string, unknown>): void;
    };
  }

  // 扩展 Array 原型
  interface Array<T> {
    groupBy<K extends string>(keyFn: (item: T) => K): Record<K, T[]>;
  }

  // 扩展 String 原型
  interface String {
    toPascalCase(): string;
  }

  // 声明全局变量
  const __DEV__: boolean;
  const __APP_VERSION__: string;
}

注意:

  1. 必须包含 export {}:否则文件被视为脚本(script),declare global 会报错。
  2. 接口合并:interface Window 会与已有的 Window 接口合并,而非覆盖。
  3. 类型与值同时声明:declare global 中可以同时声明类型(interface)与值(const/function)。

4.2 declare namespace

declare namespace 用于声明全局命名空间,主要适用于:

  1. 旧式全局库:通过 <script> 标签引入的库(如 jQuery)。
  2. 复杂的全局对象:需要嵌套结构的全局变量。
  3. 库的工具命名空间:例如 _.foo、$.ajax。
// jquery.d.ts(简化版)
declare namespace $ {
  function ajax(url: string, settings?: $.AjaxSettings): $.jqXHR;
  function get(url: string, data?: object): $.jqXHR;

  namespace $.AjaxSettings {
    type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
  }

  interface AjaxSettings {
    method?: $.AjaxSettings.Method;
    url: string;
    data?: object;
    success?: (data: any) => void;
    error?: (xhr: $.jqXHR, status: string) => void;
  }

  interface jqXHR {
    abort(): void;
    done(callback: (data: any) => void): jqXHR;
    fail(callback: (xhr: jqXHR, status: string) => void): jqXHR;
  }
}

// 使用
$.ajax('/api/users', { method: 'GET' });

现代 TypeScript 工程应尽量避免 declare namespace,改用 ES 模块。仅在与遗留代码兼容时使用。

4.3 全局变量声明模式

// 模式 1:纯全局变量
declare const __DEV__: boolean;

// 模式 2:全局对象
declare const process: {
  env: Record<string, string | undefined>;
  argv: string[];
  exit(code?: number): never;
};

// 模式 3:全局函数
declare function fetch(input: string | URL, init?: RequestInit): Promise<Response>;

// 模式 4:全局类
declare class CustomError extends Error {
  constructor(message: string, code?: number);
  readonly code?: number;
}

4.4 lib.d.ts 与 lib 选项

TypeScript 内置了一组声明文件,描述 JavaScript 标准库与 DOM API:

// tsconfig.json
{
  "compilerOptions": {
    "lib": [
      "ES2023",        // ES2023 标准库
      "DOM",           // DOM API
      "DOM.Iterable",  // DOM 迭代器
      "ScriptHost"     // Windows Script Host
    ]
  }
}

lib 选项决定 TypeScript 知道哪些全局类型。例如:

  • 不包含 "DOM" 时,document、window 等不可用。
  • 不包含 "ES2022" 时,Array.at()、Object.hasOwn() 等不可用。
  • 仅包含 "ESNext" 时,所有最新特性可用。

5. 模块声明

5.1 declare module

declare module 用于为 npm 包或相对路径模块提供类型声明。两种主要形式:

形式 1:通配符模块声明

// types/webpack-modules.d.ts
declare module '*.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.svg' {
  import React from 'react';
  export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

declare module '*.json' {
  const value: unknown;
  export default value;
}

适用于 Vite/Webpack 等打包器工程中导入非 JS 资源。

形式 2:具名模块声明

// types/untyped-lib.d.ts
declare module 'untyped-lib' {
  export interface Options {
    timeout?: number;
    retries?: number;
  }

  export function request(url: string, options?: Options): Promise<Response>;

  export class Client {
    constructor(baseURL: string);
    get<T>(url: string): Promise<T>;
    post<T>(url: string, body: unknown): Promise<T>;
  }

  export default Client;
}

为没有内置类型的 npm 包提供声明。注意:具名模块声明会完全替换包的实际类型(如果有的话)。

5.2 模块声明的查找顺序

TypeScript 查找模块类型的顺序:

  1. 包内置类型:package.json 的 types/typings 字段,或 exports.types。
  2. @types 包:node_modules/@types/<package>/index.d.ts。
  3. typeRoots 配置:tsconfig.json 中 typeRoots 指定的目录。
  4. 三斜线指令引用:通过 /// <reference types="..." /> 引入。
  5. 手写声明文件:项目内的 .d.ts 文件。

5.3 模块声明的陷阱

// 错误:模块声明与实际包冲突
declare module 'lodash' {
  export function get(obj: object, path: string): any;
}

// 实际使用时,TypeScript 优先使用上面的声明
// 即使 @types/lodash 已安装,也会被覆盖
import { get } from 'lodash';
// get 的类型是声明的版本,而非 @types/lodash 的完整版本

修复:仅在包确实没有类型时使用 declare module,否则使用 @types。


6. 声明合并规则

声明合并(declaration merging)是 TypeScript 的核心特性,允许同名的多个声明合并为一个。理解合并规则对于扩展第三方类型至关重要。

6.1 接口合并

interface Window {
  customProp: string;
}

interface Window {
  anotherProp: number;
}

// 等价于
interface Window {
  customProp: string;
  anotherProp: number;
}

接口合并规则:

  1. 非函数成员必须唯一:同名属性必须是相同类型。
  2. 函数成员重载:后声明的接口出现在重载列表前面。
  3. 后续接口必须有实现:如果第一个接口定义了属性,后续接口不能改变其类型。

6.2 命名空间合并

// 扩展命名空间
namespace MyLib {
  export const version = '1.0';
}

namespace MyLib {
  export function doSomething() { /* ... */ }
}

// 等价于
namespace MyLib {
  export const version = '1.0';
  export function doSomething() { /* ... */ }
}

6.3 命名空间与函数合并

function MyLib(options: object): void;

namespace MyLib {
  export const version = '1.0';
  export function advanced(): void;
}

// 使用
MyLib({});           // 作为函数调用
MyLib.version;       // 作为命名空间访问属性
MyLib.advanced();    // 作为命名空间调用方法

这是 jQuery 等库的常见模式:$() 是函数,$.ajax() 是方法。

6.4 命名空间与类合并

class Calculator {
  constructor(public precision: number) {}
}

namespace Calculator {
  export const defaultPrecision = 2;
  export function create(): Calculator {
    return new Calculator(defaultPrecision);
  }
}

// 使用
const calc = new Calculator(10);
const defaultCalc = Calculator.create();
console.log(Calculator.defaultPrecision);

6.5 模块扩展(declare module)

通过 declare module 可以扩展已存在模块的类型:

// types/express.d.ts
declare module 'express' {
  // 扩展 Request 接口
  interface Request {
    user?: {
      id: string;
      role: 'admin' | 'user';
    };
    traceId?: string;
  }

  // 扩展 Response 接口
  interface Response {
    success(data: unknown): void;
    fail(error: string, code?: number): void;
  }
}

使用:

import express, { Request, Response } from 'express';

const app = express();

app.get('/me', (req: Request, res: Response) => {
  // req.user 现在可用(通过声明合并)
  if (!req.user) {
    res.fail('Unauthorized', 401);
    return;
  }
  res.success({ id: req.user.id });
});

6.6 模块扩展的限制

// 错误:不能扩展模块的顶层导出
declare module 'express' {
  // 这是允许的:扩展接口
  interface Request { user?: User; }

  // 这是允许的:新增命名空间
  namespace Express {
    interface User { id: string; }
  }

  // 错误:不能添加新的顶层导出
  // export function myHelper(): void;
}

模块扩展只能扩展已有的接口或命名空间,不能添加新的顶层导出。

6.7 全局扩展

// types/global.d.ts
export {};  // 必须有 export 才是模块

declare global {
  // 扩展 Array
  interface Array<T> {
    last(): T | undefined;
    first(): T | undefined;
    chunk(size: number): T[][];
  }

  // 扩展 Promise
  interface Promise<T> {
    timeout(ms: number): Promise<T>;
  }
}

实现:

// polyfills.ts
Array.prototype.last = function () { return this[this.length - 1]; };
Array.prototype.first = function () { return this[0]; };
Array.prototype.chunk = function (size: number) {
  const result: any[][] = [];
  for (let i = 0; i < this.length; i += size) {
    result.push(this.slice(i, i + size));
  }
  return result;
};

7. 三斜线指令

7.1 三斜线指令概述

三斜线指令(triple-slash directives)是 TypeScript 早期引入的指令,用于在声明文件中显式声明依赖。现代 TypeScript 推荐使用 import 语句,但三斜线指令仍在 lib.d.ts 与 @types 包中广泛使用。

7.2 三种三斜线指令

/// <reference path="./utils.d.ts" />
/// <reference types="node" />
/// <reference lib="es2020" />

/// <reference path="..." />

显式声明对另一个声明文件的依赖。TypeScript 会按顺序加载被引用的文件:

// types/main.d.ts
/// <reference path="./utils.d.ts" />
/// <reference path="./events.d.ts" />

declare function initialize(): void;

/// <reference types="..." />

声明对 @types 包的依赖。等同于在 tsconfig.json 的 types 中包含该包:

// types/globals.d.ts
/// <reference types="node" />

declare function readFile(path: string): Buffer;  // Buffer 来自 @types/node

/// <reference lib="..." />

声明对 lib 内置包的依赖:

// types/polyfill.d.ts
/// <reference lib="es2022.array" />

declare const arr: number[];
arr.at(-1);  // ES2022 的 Array.at 方法可用

7.3 现代替代方案

现代 TypeScript 推荐使用 import 语句替代三斜线指令:

// 旧:三斜线指令
/// <reference path="./utils.d.ts" />
declare function useUtils(): void;

// 新:import 语句
import type { Utils } from './utils.js';
declare function useUtils(): Utils;

import type 是 TypeScript 3.8+ 引入的语法,仅在类型层面使用,不会生成运行时代码。

7.4 三斜线指令的使用场景

仍在使用三斜线指令的场景:

  1. lib.d.ts 内部:TypeScript 内置声明文件大量使用。
  2. @types 包内部:例如 @types/node 引用其他 @types。
  3. 生成的声明文件:--declaration 生成的 .d.ts 可能包含。
  4. 显式 lib 依赖:在不修改 tsconfig.json 的情况下引入特定 lib。

8. 模块解析策略

8.1 五种模块解析策略

TypeScript 提供五种 moduleResolution 选项:

策略引入版本适用场景关键特性
classic1.0旧 ES 模块简单相对路径解析,不查 node_modules
node (node10)1.0Node.js CJS模拟 Node.js 的 require 解析
node164.7Node.js ESM/CJS严格区分 ESM/CJS,要求扩展名
nodenext4.7Node.js ESM/CJS等同于 node16,未来指向最新
bundler5.0打包器工程模拟 Vite/Webpack 宽松解析

8.2 classic 解析策略

classic 是最简单的解析策略,仅查找相对路径与 ambient 声明:

// /src/app.ts
import { foo } from './utils';
// classic 查找:
// 1. /src/utils.ts
// 2. /src/utils.d.ts

import { bar } from 'lodash';
// classic 查找:
// 1. /src/lodash.ts
// 2. /src/lodash.d.ts
// 3. /lodash.ts
// 4. /lodash.d.ts

classic 不查 node_modules,已不推荐使用。

8.3 node (node10) 解析策略

node 模拟 Node.js Classic 的 require 解析算法:

// /src/app.ts
import { foo } from './utils';
// node 查找:
// 1. /src/utils.ts
// 2. /src/utils.tsx
// 3. /src/utils.d.ts
// 4. /src/utils/package.json (types 字段)
// 5. /src/utils/index.ts
// 6. /src/utils/index.d.ts

import { bar } from 'lodash';
// node 查找:
// 1. /src/node_modules/lodash/package.json (types 字段)
// 2. /src/node_modules/lodash/index.d.ts
// 3. /node_modules/lodash/...
// 4. /node_modules/@types/lodash/index.d.ts

node 是 TypeScript 4.6 及以前的默认策略,至今仍是最广泛使用的。

8.4 node16/nodenext 解析策略

node16(4.7+)与 nodenext(4.7+)严格对齐 Node.js 12+ 的 ESM/CJS 解析规则:

// /src/app.ts (ESM, package.json: "type": "module")
import { foo } from './utils';
// node16 查找:
// 1. /src/utils.ts → 编译为 /src/utils.js
// 必须显式写 .js 扩展名!
// 错误:import { foo } from './utils';  ← 不允许
// 正确:import { foo } from './utils.js';

node16/nodenext 的核心规则:

  1. 相对路径必须显式扩展名:ESM 模式下 ./utils 无效,必须 ./utils.js。
  2. package.json type 字段决定模式:"type": "module" 表示 ESM,否则 CJS。
  3. 支持 conditional exports:根据导入方式选择 import/require 入口。
  4. 强制区分 .ts 与 .d.ts:开发时导入 .ts,类型解析 .d.ts。

8.5 bundler 解析策略

bundler(5.0+)模拟 Vite/Webpack/Rollup 等打包器的宽松解析行为:

// bundler 模式下,以下导入都合法
import { foo } from './utils';        // 不需要扩展名
import { bar } from './utils.js';     // 也可以写 .js
import { baz } from './utils.ts';     // 部分打包器允许写 .ts
import { qux } from 'lodash';         // node_modules 解析

bundler 的关键特性:

  1. 不需要扩展名:与 node10 类似。
  2. 支持 conditional exports:与 node16 类似。
  3. 不强制 ESM/CJS 区分:打包器会处理互操作。
  4. 要求 module 设置:必须配合 "module": "esnext" 或 "preserve"。

8.6 解析策略选择决策树

flowchart TD
    T0["你的工程运行在哪?"]
    T1["Node.js(直接运行编译产物)"]
    T2["使用 ESM(type: module) → nodenext"]
    T3["使用 CJS(require) → node16 或 node10"]
    T4["双格式发布 → nodenext + 双 tsconfig"]
    T5["浏览器/打包器(Vite/Webpack/Rollup)"]
    T6["bundler"]
    T7["Deno"]
    T8["nodenext(Deno 2.0+ 兼容 npm)"]
    T9["旧项目兼容"]
    T10["node(不推荐新项目使用)"]
    T0 --> T1
    T4 --> T5
    T6 --> T7
    T8 --> T9
    T9 --> T10

8.7 实战对比

考虑以下项目结构:

flowchart TD
    T0["project/"]
    T1["src/"]
    T2["app.ts"]
    T3["utils.ts"]
    T4["utils.d.ts"]
    T5["package.json"]
    T6["tsconfig.json"]
    T0 --> T1
    T4 --> T5
    T4 --> T6

不同解析策略下 import { x } from './utils' 的行为:

策略是否需要扩展名优先加载 .ts 还是 .d.ts
classic否.ts
node (node10)否.ts
node16 (ESM)是(.js).ts
nodenext (ESM)是(.js).ts
bundler否.ts

9. 路径映射与 baseUrl

9.1 baseUrl

baseUrl 设置模块解析的根目录。设置后,非相对路径导入会从 baseUrl 开始查找:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}
typescript
// 等同于从 ./src/utils 导入
import { foo } from 'utils';

注意:baseUrl 主要用于历史兼容,现代项目推荐使用 paths 替代。

9.2 paths

paths 是更灵活的路径映射机制,支持别名与模式匹配:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils": ["src/utils/index.ts"],
      "@lib/*": ["libs/*", "node_modules/*"]
    }
  }
}
typescript
import { foo } from '@utils';             // src/utils/index.ts
import { Button } from '@components/Button';  // src/components/Button.ts
import { fetch } from '@lib/api';         // 先查 libs/api,再查 node_modules/api

9.3 paths 的运行时问题

paths 仅是 TypeScript 编译时的别名映射,运行时不会自动转换。需要在打包器或运行时配置中同步映射:

Vite 配置

// vite.config.ts
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
    },
  },
});

Webpack 配置

// webpack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
    },
  },
};

tsconfig-paths(Node.js 运行时)

npm install --save-dev tsconfig-paths
node -r tsconfig-paths/register dist/index.js

ts-node 配置

// tsconfig.json
{
  "ts-node": {
    "files": true,
    "transpileOnly": true
  },
  "compilerOptions": {
    "paths": { "@/*": ["src/*"] }
  }
}

9.4 paths 与 monorepo

monorepo 中常使用 paths 跨包引用:

// packages/web/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@myorg/shared": ["../shared/src/index.ts"],
      "@myorg/shared/*": ["../shared/src/*"]
    }
  }
}

但更推荐使用 project references(references 字段):

{
  "references": [
    { "path": "../shared" }
  ]
}

10. package.json 的 exports 字段

10.1 exports 字段概述

Node.js 12+ 引入 exports 字段,用于:

  1. 限制包的可导入路径(封装包内部)。
  2. 提供多入口(main、module、browser、types)。
  3. 条件导出(ESM/CJS/不同环境)。
{
  "name": "my-lib",
  "version": "1.0.0",
  "main": "./dist/index.cjs",         // CJS 入口(旧 Node)
  "module": "./dist/index.mjs",        // ESM 入口(打包器)
  "types": "./dist/index.d.ts",        // 类型入口(旧 TypeScript)
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  }
}

10.2 types 条目的位置

关键规则:在 conditional exports 中,types 必须放在最前面:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",  // 必须在最前
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

原因:TypeScript 解析 exports 时使用 first-match 策略。如果 import 在 types 之前,TypeScript 可能匹配到 .mjs 入口但找不到对应 .d.ts。

10.3 子路径导出

{
  "name": "my-lib",
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js",
    "./utils/*": "./dist/utils/*.js",
    "./internal/*": null  // 禁止访问内部模块
  }
}
typescript
import { x } from 'my-lib';           // 解析到 ./dist/index.js
import { y } from 'my-lib/utils';     // 解析到 ./dist/utils.js
import { z } from 'my-lib/utils/foo'; // 解析到 ./dist/utils/foo.js
import { w } from 'my-lib/internal/secret';  // 错误:禁止访问

10.4 条件导出

Node.js 定义的标准条件:

条件含义
importESM 导入
requireCJS require
nodeNode.js 环境
denoDeno 环境
browser浏览器环境
default兜底条件
{
  "exports": {
    ".": {
      "node": {
        "import": "./dist/node.mjs",
        "require": "./dist/node.cjs"
      },
      "browser": {
        "import": "./dist/browser.mjs",
        "require": "./dist/browser.cjs"
      },
      "default": "./dist/index.js"
    }
  }
}

10.5 TypeScript 对 exports 的解析

TypeScript 4.7+ 完整支持 exports 字段解析。配合 moduleResolution: node16/nodenext/bundler,TypeScript 会:

  1. 读取 exports.types 条目作为类型入口。
  2. 根据 import/require 选择运行时入口。
  3. 支持子路径导出与条件导出。

10.6 双格式发布的完整配置

{
  "name": "@myorg/lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    },
    "./package.json": "./package.json"
  },
  "files": [
    "dist",
    "src"
  ],
  "sideEffects": false,
  "engines": {
    "node": ">=14.0.0"
  }
}

11. ESM 与 CJS 互操作

11.1 互操作的核心问题

ESM 与 CJS 的根本差异:

特性ESMCJS
语法import/exportrequire/module.exports
加载异步、静态分析同步、动态
this 顶层undefinedmodule.exports
__dirname不可用可用
require不可用可用
import.meta.url可用不可用
默认导出export defaultmodule.exports = 或 module.exports.default =

11.2 esModuleInterop

esModuleInterop 是 TypeScript 解决 ESM/CJS 互操作的核心选项。它允许 TypeScript 在导入 CJS 模块时模拟 ESM 的默认导入语义:

// 不启用 esModuleInterop
import * as express from 'express';
const app = express();

// 启用 esModuleInterop
import express from 'express';
const app = express();

esModuleInterop 启用后,TypeScript 会生成额外的辅助函数:

// 编译产物
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const app = (0, express_1.default)();

11.3 allowSyntheticDefaultImports

allowSyntheticDefaultImports 仅影响类型检查,不影响运行时编译:

  • 不启用:导入 CJS 模块时 import x from 'cjs-module' 类型错误。
  • 启用:类型检查通过,但运行时仍需 esModuleInterop 或打包器处理。

esModuleInterop: true 自动启用 allowSyntheticDefaultImports: true。

11.4 互操作场景与配置矩阵

项目格式导入 CJS导入 ESM推荐 tsconfig
CJS (require)直接 require(x)使用 dynamic import()module: commonjs, esModuleInterop: true
ESM (import)需 esModuleInterop 或 default 导入直接 import x from 'y'module: nodenext, esModuleInterop: true
打包器直接 import x from 'y'直接 import x from 'y'module: esnext, moduleResolution: bundler

11.5 常见互操作错误

// 错误 1:在 ESM 中直接 require
// ESM 文件不能使用 require
import { x } from 'cjs-lib';
const y = require('cjs-lib'); // SyntaxError in ESM

// 修复:使用 createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const y = require('cjs-lib');
typescript
// 错误 2:CJS 中导入 ESM(Node.js 22+ 仍不支持)
const esmMod = require('esm-lib'); // Error: require() of ES Module

// 修复:使用 dynamic import(async)
async function loadEsm() {
  const esmMod = await import('esm-lib');
  return esmMod;
}

11.6 __dirname 与 __filename 在 ESM 中的替代

// ESM 中无 __dirname 和 __filename
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

11.7 import type 与 type-only imports

TypeScript 3.8+ 引入 import type,仅在类型层面使用,不会生成运行时代码:

import type { SomeType } from 'some-lib';
import { someValue } from 'some-lib';

// 编译后 only someValue 保留

TypeScript 4.5+ 支持内联 type 限定:

import { someValue, type SomeType } from 'some-lib';

verbatimModuleSyntax 选项(TS 5.0+)强制所有类型导入必须显式标注:

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}
typescript
// 必须显式 type
import { type Type1, value1 } from 'lib';  // 正确
import { Type1, value1 } from 'lib';        // 错误:Type1 必须标注 type

12. DefinitelyTyped 与 @types

12.1 DefinitelyTyped 项目

DefinitelyTyped 是 GitHub 上最大的 TypeScript 声明文件仓库,由社区维护,提供数千个 npm 包的类型声明。所有 @types/* 包都从 DefinitelyTyped 仓库自动发布。

仓库地址:https://github.com/DefinitelyTyped/DefinitelyTyped

12.2 安装 @types

# 安装某个包的类型
npm install --save-dev @types/lodash

# 安装多个包的类型
npm install --save-dev @types/node @types/jest @types/react

# 查看是否有 @types 包
npm info @types/your-package

12.3 types 与 typeRoots

{
  "compilerOptions": {
    // 显式指定包含的 @types 包(默认包含所有)
    "types": ["node", "jest", "react"],

    // 自定义 typeRoots 路径
    "typeRoots": ["./node_modules/@types", "./types"]
  }
}

types 配置的影响:

  • 不设置:自动包含 node_modules/@types 下所有包。
  • 设置为空数组:不自动包含任何 @types 包。
  • 设置为列表:仅包含列表中的包。
// 仅包含 node 类型,其他 @types 不自动加载
{
  "compilerOptions": {
    "types": ["node"]
  }
}

12.4 @types 包的结构

一个典型的 @types 包结构:

flowchart TD
    T0["@types/lodash/"]
    T1["package.json"]
    T2["index.d.ts"]
    T3["other-utils.d.ts"]
    T4["tsconfig.json  (DefinitelyTyped 配置)"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T0 --> T4
jsonc
// @types/lodash/package.json
{
  "name": "@types/lodash",
  "version": "4.14.0",
  "description": "TypeScript definitions for lodash",
  "main": "index.d.ts",
  "types": "index.d.ts"
}

12.5 自己编写 @types 包

如果 npm 包没有 @types,可以自己编写:

// types/my-lib/index.d.ts
declare module 'my-lib' {
  export interface Options {
    timeout?: number;
  }

  export function doSomething(value: string, options?: Options): Promise<string>;
  export default function doSomethingDefault(): void;
}
jsonc
// tsconfig.json
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  }
}

12.6 发布 @types 包

如果觉得自己的类型声明对社区有用,可以提交到 DefinitelyTyped:

  1. Fork DefinitelyTyped 仓库。
  2. 在 types/<package-name>/ 下创建声明文件。
  3. 添加 tsconfig.json 与 tslint.json。
  4. 提交 PR,等待审核与发布。

13. 对比分析

13.1 与 Python 类型系统的对比

Python 通过 py.typed 标记与 PEP 561 提供类型支持,机制类似 TypeScript:

# Python 包内联类型
# my_package/__init__.py
def hello(name: str) -> str:
    return f"Hello, {name}"

# my_package/py.typed  # 空文件,标记支持类型

差异:

  • TypeScript 声明与实现分离(.d.ts vs .ts)。
  • Python 类型与实现同文件,通过 stub 文件(.pyi)支持分离。
  • Python 的类型检查器(mypy/pyright)独立于运行时。

13.2 与 Rust 模块系统的对比

Rust 的模块系统更严格:

// Rust 模块声明
mod utils;
use crate::utils::helper;

// 必须在 lib.rs 或 main.rs 显式声明模块
// 不存在自动模块发现

差异:

  • Rust 模块基于文件系统但需要显式声明。
  • TypeScript 模块基于文件系统,自动发现。
  • Rust 的 crate(包)系统更接近 monorepo。

13.3 与 Go 模块系统的对比

Go 的模块系统简化:

// Go 模块导入
import (
    "fmt"
    "github.com/user/repo/pkg"
)

// 包名与导入路径的最后一段相同
// 不需要扩展名
// 没有 .d.ts 等类型声明文件(类型与实现同文件)

差异:

  • Go 类型与实现同文件,不需要声明文件。
  • Go 模块路径基于 URL(github.com/user/repo)。
  • Go 没有 @types 等价物。

13.4 综合对比

特性TypeScriptPythonRustGo
声明文件.d.ts.pyi无无
模块解析多策略sys.pathCargo + modURL + GOPATH
类型与实现分离支持支持(stub)不支持不支持
包生态npm + @typesPyPIcrates.iopkg.go.dev
类型检查器tscmypy/pyrightrustcgo vet

14. 常见陷阱与修复

14.1 陷阱 1:NodeNext 下未使用扩展名

// 错误
import { foo } from './utils';

// 修复
import { foo } from './utils.js';

NodeNext 要求相对路径导入必须显式写 .js 扩展名(即使源文件是 .ts)。

14.2 陷阱 2:找不到模块声明

import { x } from 'untyped-lib';
// Error: Could not find a declaration file for module 'untyped-lib'.

修复方案:

// 方案 1:安装 @types
npm install --save-dev @types/untyped-lib

// 方案 2:自己写声明文件
// types/untyped-lib.d.ts
declare module 'untyped-lib' {
  export function x(): void;
}

// 方案 3:使用 ts-ignore 绕过(不推荐)
// @ts-ignore
import { x } from 'untyped-lib';

14.3 陷阱 3:types 条目顺序错误

// 错误:types 在 import 后
{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"  // 不会被匹配
    }
  }
}

修复:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",  // 必须在最前
      "import": "./dist/index.mjs"
    }
  }
}

14.4 陷阱 4:declare module 覆盖 @types

// 错误:declare module 覆盖 @types/lodash
declare module 'lodash' {
  export function get(obj: object, path: string): any;
}

import _ from 'lodash';
// _.get 类型是上面的声明,而非 @types/lodash 的完整版本

修复:删除 declare module,使用 @types/lodash。

14.5 陷阱 5:路径映射未在运行时同步

// tsconfig.json paths 配置 @/components → src/components
import { Button } from '@/components/Button';
// TypeScript 通过,但运行时找不到 @/components

修复:在打包器或运行时配置中同步别名映射(见第 9.3 节)。

14.6 陷阱 6:循环导入的类型丢失

// a.ts
import { B } from './b';
export interface A { b: B; }

// b.ts
import { A } from './a';
export interface B { a: A; }

修复:使用 import type 仅导入类型,避免运行时循环:

// a.ts
import type { B } from './b';
export interface A { b: B; }

// b.ts
import type { A } from './a';
export interface B { a: A; }

14.7 陷阱 7:lib.d.ts 冲突

// 错误:自定义类型与 lib.d.ts 冲突
interface Array<T> {
  // 与 ES2022 的 Array.at 冲突
  at(index: number): T | undefined;
}

修复:使用 lib 选项选择标准库版本,避免手动重复定义。

14.8 陷阱 8:跳过 lib 检查导致的错误

{
  "compilerOptions": {
    "skipLibCheck": true  // 跳过 .d.ts 检查
  }
}

skipLibCheck 会跳过所有 .d.ts 文件的类型检查,可能隐藏第三方库的类型错误。仅在大型项目性能优化时使用。


15. 工程实践

15.1 现代项目 tsconfig.json 模板

打包器工程(Vite/Webpack)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src", "types"],
  "exclude": ["node_modules", "dist"]
}

Node.js ESM 工程

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2023"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

库工程(双格式发布)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2020"],
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "outDir": "./dist/types",
    "rootDir": "./src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

15.2 双格式发布工程结构

flowchart TD
    T0["my-lib/"]
    T1["src/"]
    T2["index.ts"]
    T3["utils.ts"]
    T4["tsconfig.json"]
    T5["tsup.config.ts          # 使用 tsup 构建双格式"]
    T6["package.json"]
    T7["README.md"]
    T0 --> T1
    T3 --> T4
    T3 --> T5
    T3 --> T6
    T3 --> T7
typescript
// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts', 'src/utils.ts'],
  format: ['cjs', 'esm'],
  dts: true,                  // 生成 .d.ts
  splitting: false,
  sourcemap: true,
  clean: true,
  treeshake: true,
});
jsonc
// package.json
{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  },
  "files": ["dist"],
  "sideEffects": false,
  "engines": {
    "node": ">=14"
  }
}

15.3 monorepo 模块解析

flowchart TD
    T0["monorepo/"]
    T1["packages/"]
    T2["shared/"]
    T3["src/"]
    T4["index.ts"]
    T5["tsconfig.json"]
    T6["package.json"]
    T7["web/"]
    T8["src/"]
    T9["tsconfig.json"]
    T10["package.json"]
    T11["api/"]
    T12["src/"]
    T13["tsconfig.json"]
    T14["package.json"]
    T15["tsconfig.base.json"]
    T16["tsconfig.json"]
    T17["package.json"]
    T0 --> T1
    T14 --> T15
    T14 --> T16
    T14 --> T17
jsonc
// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}
jsonc
// packages/shared/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}
jsonc
// packages/web/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "paths": {
      "@myorg/shared": ["../shared/src"],
      "@myorg/shared/*": ["../shared/src/*"]
    }
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src"]
}

15.4 project references 实战

// 顶层 tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/web" },
    { "path": "./packages/api" }
  ]
}

使用 tsc --build 增量编译:

tsc --build                # 构建所有引用的项目
tsc --build --watch        # 增量监听
tsc --build --force        # 强制重新构建
tsc --build --clean        # 清理构建产物

15.5 自定义类型声明组织

flowchart TD
    T0["project/"]
    T1["src/"]
    T2["types/"]
    T3["global.d.ts        # 全局声明"]
    T4["assets.d.ts        # 静态资源声明(*.css, *.png)"]
    T5["modules.d.ts       # 第三方模块声明"]
    T6["express.d.ts       # Express 扩展声明"]
    T7["tsconfig.json"]
    T8["package.json"]
    T0 --> T1
    T0 --> T2
    T6 --> T7
    T6 --> T8
jsonc
// tsconfig.json
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": ["src", "types"]
}

15.6 类型检查脚本

// package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch",
    "build:types": "tsc --emitDeclarationOnly",
    "build": "tsc && vite build"
  }
}

16. 案例研究

16.1 案例一:Vite + React 项目

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
jsonc
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

16.2 案例二:Node.js ESM 服务

// package.json
{
  "name": "my-api",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
jsonc
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2023"],
    "strict": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}
typescript
// src/index.ts
import express from 'express';
import { router } from './routes.js';  // 必须显式 .js

const app = express();
app.use(router);
app.listen(3000);

16.3 案例三:npm 库双格式发布

// src/index.ts
export class MyClass {
  constructor(public value: number) {}

  double(): number {
    return this.value * 2;
  }
}

export function helper(x: string): string {
  return x.toUpperCase();
}
jsonc
// package.json
{
  "name": "@myorg/lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "prepublishOnly": "npm run build"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.4.0"
  }
}

16.4 案例四:扩展 Express 类型

// types/express.d.ts
declare module 'express' {
  interface Request {
    user?: {
      id: string;
      role: 'admin' | 'user';
    };
    traceId?: string;
  }

  interface Response {
    success<T>(data: T): void;
    fail(error: string, code?: number): void;
  }
}
typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';

export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  // req.user 类型可用
  if (!req.user) {
    res.fail('Unauthorized', 401);  // res.fail 可用
    return;
  }
  next();
}

16.5 案例五:Webpack 静态资源声明

// types/assets.d.ts
declare module '*.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.jpg' {
  const src: string;
  export default src;
}

declare module '*.svg' {
  import * as React from 'react';
  export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

declare module '*.svg?url' {
  const src: string;
  export default src;
}

declare module '*.woff' {
  const src: string;
  export default src;
}

declare module '*.woff2' {
  const src: string;
  export default src;
}

16.6 案例六:Vue SFC 声明

// types/vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
typescript
// 使用
import MyComponent from './MyComponent.vue';
// MyComponent 类型为 DefineComponent

16.7 案例七:JSON 模块导入

// tsconfig.json
{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}
typescript
// config.json
{
  "appName": "MyApp",
  "version": "1.0.0"
}

// app.ts
import config from './config.json';
console.log(config.appName);  // 类型推导为 string

16.8 案例八:动态导入

// 动态导入返回 Promise<T>
const module = await import('./utils.js');
// module 类型为 typeof import('./utils.js')

// 配合代码分割
const routes = {
  home: () => import('./pages/Home.js'),
  about: () => import('./pages/About.js'),
  contact: () => import('./pages/Contact.js'),
};

type RouteLoader = () => Promise<typeof import('./pages/Home.js')>;

填空题知识点讲解

  1. [remember] TypeScript 5.0+ 推荐的现代模块解析策略中,____适合打包器工程,____适合 Node.js ESM 工程。

  2. [remember] 在 NodeNext 解析策略下,相对路径导入必须显式包含____扩展名,且 .ts 源文件对应的运行时导入路径应写为____。

  3. [understand] package.json 的 exports 字段中,____条件用于声明 TypeScript 类型入口,其值必须以____开头。

  4. [understand] 声明合并规则中,接口的____成员必须唯一且类型一致,____成员会按声明顺序合并为重载列表。

  5. [remember] TypeScript 内置的声明文件位于 ____目录,可通过 ____ 选项配置包含哪些标准库。

17.3 代码修复题

  1. [apply] 以下 NodeNext 工程代码报错 “Could not find a declaration file for module ’./utils’“。请修复导入语句。

    // src/index.ts
    import { helper } from './utils';

    修复方案:

    // NodeNext 要求相对路径导入显式写 .js 扩展名
    import { helper } from './utils.js';
  2. [apply] 以下 Express 扩展声明无法生效,请修复。

    // types/express.d.ts
    declare module 'express' {
      export function myHelper(): void;
    }

    修复方案:

    // 错误:模块扩展不能添加新的顶层导出,只能扩展现有接口
    declare module 'express' {
      interface Request {
        user?: { id: string; role: 'admin' | 'user' };
      }
    }
  3. [apply] 以下 package.json exports 配置导致 TypeScript 无法找到类型,请修复。

    {
      "exports": {
        ".": {
          "import": "./dist/index.mjs",
          "require": "./dist/index.cjs",
          "types": "./dist/index.d.ts"
        }
      }
    }

    修复方案:

    {
      "exports": {
        ".": {
          "types": "./dist/index.d.ts",
          "import": "./dist/index.mjs",
          "require": "./dist/index.cjs"
        }
      }
    }
  4. [apply] 以下声明文件在模块文件中扩展 Window 失败,请修复。

    // types/globals.d.ts
    declare global {
      interface Window {
        myApp: { version: string };
      }
    }

    修复方案:

    // 必须有 export {} 才是模块文件
    export {};
    
    declare global {
      interface Window {
        myApp: { version: string };
      }
    }

17.4 开放题

  1. [evaluate] 你正在为一个同时支持 ESM 与 CJS 双格式发布的 npm 包编写 package.json。请描述 exports 字段的完整结构,包括 main、module、import、require、types 条目,并说明为什么 types 条目必须放在最前面。

  2. [create] 设计一个 monorepo 项目的模块解析策略,要求:

    • 使用 pnpm workspaces
    • 包含 shared、web、api 三个包
    • shared 包被 web 和 api 共享
    • web 使用 bundler 解析,api 使用 NodeNext 解析
    • 提供 tsconfig.base.json 与各包的 tsconfig.json
    • 描述构建与开发流程
  3. [analyze] 阅读以下错误信息,分析根本原因并给出三种可能的修复方案:

    Error: Could not find a declaration file for module 'my-lib'.
    'my-lib/index.js' implicitly has an 'any' type.
  4. [create] 为一个使用 Vite + React + TypeScript 的项目设计完整的类型声明组织,包括:

    • 静态资源(CSS/图片/SVG)
    • 环境变量(import.meta.env)
    • 全局扩展(Window 接口)
    • 第三方库扩展(React 组件库)
    • 路由配置类型
  5. [evaluate] 对比 moduleResolution: node、node16、nodenext、bundler 四种策略的优劣,并说明在什么场景下应该选择哪种。


19.1 书籍

  • 《Programming TypeScript》——Boris Cherny,O’Reilly 2019。第 9 章对模块解析有详尽讲解。
  • 《Effective TypeScript》——Dan Vanderkam,O’Reilly 2019。第 7 章包含模块配置实践。
  • 《Learning TypeScript》——Josh Goldberg,O’Reilly 2022。第 8 章涵盖声明文件编写。
  • 《TypeScript Cookbook》——Stefan Baumgartner,O’Reilly 2023。
  • 《Elevate Web Apps with TypeScript》——Yvonne Zhang,Manning 2024。

19.2 论文

  • Bierman, G., Abadi, M., and Torgersen, M. 2014. Understanding TypeScript. ECOOP 2014.
  • Guarneri, S. and Gardner, P. 2021. A formal semantics for ES modules. ESOP 2021.
  • Bradley, M. and Bonsangue, M. 2018. A formal semantics for the JavaScript module system. FESCA 2018.

19.3 开源项目

19.5 视频课程

19.6 工具


附录 A:模块解析策略对比表

特性classicnode (node10)node16nodenextbundler
引入版本1.01.04.74.75.0
相对路径扩展名可省可省必须显式必须显式可省
node_modules 查找否是是是是
exports 字段否否是是是
ESM/CJS 区分否否是是否
conditional exports否否是是是
推荐不推荐旧项目Node.jsNode.js 现代项目打包器

附录 B:常见 tsconfig.json 选项速查

选项类型作用
moduleenum模块系统:commonjs/esnext/nodenext
moduleResolutionenum解析策略
baseUrlstring路径映射根
pathsobject路径别名映射
typesarray显式指定 @types 包
typeRootsarray@types 搜索路径
libarray内置标准库
esModuleInteropbooleanESM/CJS 互操作辅助
allowSyntheticDefaultImportsboolean允许合成默认导入
resolveJsonModuleboolean允许导入 JSON
isolatedModulesboolean单文件编译模式
verbatimModuleSyntaxboolean强制显式 type 导入
declarationboolean生成 .d.ts
declarationMapboolean生成 .d.ts.map
emitDeclarationOnlyboolean仅生成声明
skipLibCheckboolean跳过 .d.ts 检查

附录 C:错误信息索引

错误码错误信息常见原因
TS2307Cannot find module ‘X’ or its corresponding type declarations.模块未安装或声明文件缺失
TS2688Cannot find type definition file for ‘X’.@types/X 未安装或 types 配置错误
TS2459Module ‘X’ declares ‘X’ locally, but it is not exported.导入路径与实际导出不匹配
TS5097An import path cannot end with a ‘.ts’ extension.导入路径不能以 .ts 结尾(除 Bundler)
TS2835Relative import paths need explicit file extensions.NodeNext 要求显式扩展名
TS1511Module ‘X’ has no default export.esModuleInterop 未启用
TS2614Module ‘X’ can only be default-imported using esModuleInterop.esModuleInterop 未启用

附录 D:决策流程图

flowchart TD
    T0["项目类型?"]
    T1["库(被其他项目使用)"]
    T2["单格式 → 选择 ESM 或 CJS,配置 declaration: true"]
    T3["双格式 → tsup + exports 字段"]
    T4["应用(直接运行)"]
    T5["Node.js → moduleResolution: NodeNext"]
    T6["浏览器 → moduleResolution: Bundler"]
    T7["桌面(Electron) → 混合,需要多个 tsconfig"]
    T8["Monorepo"]
    T9["pnpm workspaces → 各包独立 tsconfig + project references"]
    T10["npm workspaces → 同上"]
    T0 --> T1
    T3 --> T4
    T7 --> T8
    T8 --> T9
    T8 --> T10

模块基础

基本写法:导出 export <声明>

// 导出变量函数类型等
export const name = "Tom"
export function greet() {}
export type User = { name: string }

基本写法:默认导出 export default <声明>

// 每个模块只能有一个默认导出
export default class User {}

基本写法:命名导入 import { <名称>, <名称> } from "<模块>"

// 按名导入多个
import { name, greet } from "./user"

基本写法:默认导入 import <名称> from "<模块>"

// 导入默认导出
import User from "./User"

基本写法:别名导入 import { <名称> as <别名> } from "<模块>"

// 重命名导入避免冲突
import { name as userName } from "./user"

基本写法:命名空间导入 import * as <名称> from "<模块>"

// 整体导入为一个对象
import * as utils from "./utils"
utils.format()

模块声明

基本写法:声明模块 declare module "<模块名>"

// 为 JS 模块补类型声明
declare module "my-lib" {
    export function greet(name: string): string
    export const version: string
}

基本写法:通配符模块声明 declare module "*<后缀>"

// 处理非 JS 资源导入
declare module "*.css" {
    const content: string
    export default content
}
declare module "*.png" {
    const src: string
    export default src
}

基本写法:声明全局变量 declare const <变量>: <类型>

// 声明全局变量类型
declare const VERSION: string
declare const __DEV__: boolean

基本写法:声明全局函数 declare function <名称>(<参数>): <返回类型>

// 声明全局函数
declare function $(selector: string): HTMLElement

namespace 命名空间

基本写法:定义命名空间 namespace <名称> { }

// 命名空间组织相关类型
namespace App {
    export function init() {}
    export const version = "1.0"
}
App.init()

基本写法:嵌套命名空间 namespace <外层>.<内层> { }

// 命名空间嵌套
namespace App.Config {
    export const port = 3000
}
App.Config.port

基本写法:命名空间与模块结合 export namespace <名称> { }

// 模块中导出命名空间
export namespace Utils {
    export function format(s: string) { return s.trim() }
}

声明合并

基本写法:同名接口合并 interface <名称> { }

// 同名接口自动合并
interface User { name: string }
interface User { age: number }
const u: User = { name: "T", age: 18 }

基本写法:同名命名空间合并 namespace <名称> { }

// 命名空间自动合并
namespace App { export const a = 1 }
namespace App { export const b = 2 }
App.a; App.b

基本写法:函数与接口合并 function <函数>(); interface <函数> { }

// 函数声明可与接口合并添加属性
function greet(name: string): string
namespace greet {
    export const version = "1.0"
}
greet.version

全局声明

基本写法:global 声明 declare global { }

// 在模块中扩展全局
declare global {
    interface Window { myApp: any }
}
window.myApp = {}

基本写法:扩展全局接口 declare global { interface <名称> { } }

// 扩展内置全局接口
declare global {
    interface Array<T> { last(): T | undefined }
}
Array.prototype.last = function () { return this[this.length - 1] }

模块扩展

基本写法:扩展模块声明 declare module "<模块>" { interface <名称> { } }

// 扩展已存在模块的类型
declare module "express" {
    interface Request { user?: User }
}

基本写法:扩展 Express 类型 declare module "express" { interface Request { } }

// 给 Express Request 添加属性
declare module "express-serve-static-core" {
    interface Request { userId: string }
}

ambient 声明

基本写法:声明文件 <文件>.d.ts

// 声明文件仅类型不产生代码
// types.d.ts
declare module "lib" {
    export function fn(): void
}

基本写法:声明类型别名 declare type <名称> = <类型>

// 全局类型别名声明
declare type ID = string | number

基本写法:声明枚举 declare enum <名称> { }

// 声明外部枚举
declare enum Color { Red, Green, Blue }

三斜线指令

基本写法:引用类型声明 /// <reference types="<包>" />

// 引入 @types 包
/// <reference types="node" />

基本写法:引用路径 /// <reference path="<文件>" />

// 引入指定声明文件
/// <reference path="./types.d.ts" />

基本写法:引用库 /// <reference lib="<库>" />

// 引入内置 lib
/// <reference lib="es2017" />

类型与值导入

基本写法:import type import type { <类型> } from "<模块>"

// 仅导入类型编译时移除
import type { User } from "./types"

基本写法:内联 type 限定 import { type <类型>, <值> } from "<模块>"

// 混合导入时标记类型
import { type User, getUser } from "./user"

基本写法:export type export type { <类型> }

// 仅导出类型
export type { User } from "./types"

CommonJS 互操作

基本写法:导入 CommonJS 模块 import <名称> = require("<模块>")

// CommonJS 模块导入
import fs = require("fs")

基本写法:导出 CommonJS export = <对象>

// CommonJS 风格导出
class User {}
export = User

基本写法:esModuleInterop import <名称> from "<CommonJS模块>"

// 开启 esModuleInterop 后默认导入
import fs from "fs"

动态导入

基本写法:动态 import 类型 const <模块> = await import("<模块>")

// 动态导入类型为 Promise<typeof import>
const mod = await import("./user")
mod.greet()

基本写法:动态导入类型 type <类型> = typeof import("<模块>")

// 推导模块类型
type UserModule = typeof import("./user")

实用模式

基本写法:barrel 导出 export * from "<模块>"

// index.ts 汇总导出
export * from "./user"
export * from "./post"
export * from "./comment"

基本写法:选择性 barrel export { <名称>, <名称> } from "<模块>"

// 选择性重新导出
export { User, getUser } from "./user"
export type { UserProps } from "./user"

基本写法:声明 JSON 模块 declare module "*.json"

// 允许 import JSON
declare module "*.json" {
    const value: any
    export default value
}

基本写法:环境变量类型 interface ImportMetaEnv { }

// Vite 环境变量类型
interface ImportMetaEnv {
    readonly VITE_API: string
}
interface ImportMeta {
    readonly env: ImportMetaEnv
}

模块解析

基本写法:bundler 解析策略(现代构建工具推荐) "moduleResolution": "bundler"

// tsconfig 配置 bundler 解析
// 允许省略扩展名,支持 package.json exports

基本写法:bundler 解析策略 "moduleResolution": "bundler"

// TS 5.0+ 适配打包工具的解析
// 支持 package.json exports 字段

基本写法:paths 路径映射 "paths": { "<别名>": ["<路径>"] }

// tsconfig 配置路径别名
{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": { "@/*": ["src/*"] }
    }
}

注意事项

基本写法:模块与脚本区分 export <声明> 或 import <名称>

// 含 import export 的是模块
// 否则是脚本全局可见

基本写法:isolatedModules "isolatedModules": true

// 单文件转译模式约束
// 要求类型导入显式标注
export type { User }