Agent X-Ray
NotesSkillsAbout
Notes/代码工程/TypeScript 深度教程/第6章

第6章:模块化工程 —— tsconfig、模块、声明文件与迁移

4 分钟 · 更新于 2026-09-01

第6章:模块化工程 —— tsconfig、模块、声明文件与迁移

从单文件进入真实项目后,难点从语法转向边界:哪些文件属于项目、模块怎样解析、类型从哪里来、第三方 JavaScript 如何接入,以及旧项目如何渐进迁移。


一、ES 模块是默认选择

ts
// user.ts
export interface User {
  id: string;
  name: string;
}

export function createUser(id: string, name: string): User {
  return { id, name };
}
ts
// main.ts
import { createUser, type User } from "./user.js";

const user: User = createUser("1", "Ada");

import type 明确导入只用于类型,可帮助理解类型擦除并减少某些构建配置下的运行时导入。

二、默认导出还是命名导出

命名导出通常更适合大型项目:

  • 导入名稳定。
  • IDE 自动导入更明确。
  • 一个模块可公开多个相关能力。
  • 重构时更容易全局查找。

默认导出适合模块确实只有一个主要实体、框架约定或既有生态。团队应统一,不必绝对化。

三、理解 tsconfig 的层次

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "sourceMap": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["dist", "node_modules"]
}

需要区分:

  1. 文件集合filesincludeexclude
  2. 语言输出targetlib
  3. 模块系统modulemoduleResolution
  4. 类型规则strict 及其子项。
  5. 产物outDirdeclarationsourceMap
  6. 性能incrementalskipLibCheck、项目引用。

四、模块解析必须匹配运行环境

常见组合:

  • Node.js ESM/CJS:优先理解 NodeNext
  • 由现代 bundler 处理:可使用匹配工具链的 bundler 解析模式。
  • 库发布:必须同时考虑 package.jsontypeexports、类型声明和消费端。

模块错误常常不是 TypeScript 语法问题,而是以下约定不一致:

text
源码 import 写法
↕
tsconfig module/moduleResolution
↕
package.json type/exports
↕
Node 或 bundler 的真实解析规则

五、路径映射不是运行时能力

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/*"]
    }
  }
}
ts
import { UserService } from "@app/services/user-service";

paths 主要告诉 TypeScript 如何解析类型,不保证 Node.js 或浏览器能直接理解。Bundler、测试工具和运行环境必须配置对应别名,否则会“类型检查通过、运行时报找不到模块”。

六、命名空间与模块

历史代码或无模块脚本可能使用 namespace

ts
namespace Geometry {
  export interface Point { x: number; y: number }
}

现代应用一般优先 ES 模块。命名空间仍可能出现在:

  • 老式全局脚本。
  • 某些声明文件。
  • 需要声明合并的库类型。

不要在新模块化项目中同时建立两套组织体系。

七、声明文件 .d.ts

声明文件描述现有 JavaScript 的类型,不提供实现:

ts
// legacy-calculator.d.ts
export function add(left: number, right: number): number;

常见来源:

  • 包自己附带类型。
  • @types/* 社区声明。
  • 项目本地声明。
  • 全局环境声明。

声明写错会让编译器相信错误事实,因此应以真实运行时 API 为准,并配套类型测试或集成测试。

八、为无类型模块补声明

ts
declare module "legacy-sdk" {
  export interface ClientOptions {
    endpoint: string;
  }

  export class Client {
    constructor(options: ClientOptions);
    request(path: string): Promise<unknown>;
  }
}

不要一开始写:

ts
declare module "legacy-sdk";

这会把整个模块视为 any。可以临时使用,但必须登记后续精化任务。

九、从 JavaScript 渐进迁移

一种稳妥路径:

  1. 建立 tsconfig.json
  2. 开启 allowJs,暂时允许 JS 与 TS 共存。
  3. 使用 checkJs// @ts-check 检查关键 JS。
  4. 用 JSDoc 补充边界类型。
  5. 从无依赖工具模块和领域模型开始改 .ts
  6. 逐步开启严格选项。
  7. 清理 any@ts-ignore 和宽泛声明。

JSDoc 示例:

js
// @ts-check
/**
 * @param {number[]} values
 * @returns {number}
 */
export function total(values) {
  return values.reduce((sum, value) => sum + value, 0);
}

十、抑制错误的工具

  • @ts-expect-error:期望下一行存在错误;错误消失时会提醒,优于永久忽略。
  • @ts-ignore:无条件忽略,容易留下沉默债务。
  • as unknown as T:强行穿透检查,应极少使用。
  • skipLibCheck:跳过声明文件内部检查,不等于跳过自己代码的类型安全。

推荐给每次抑制添加原因和清理条件。

十一、边界设计

模块不应只按“controllers/services/utils”机械分层,也可按领域能力组织:

text
src/
├── booking/
│   ├── model.ts
│   ├── service.ts
│   └── api.ts
├── payment/
└── shared/

公共模块需要明确:

  • 哪些类型和函数是稳定 API。
  • 哪些只是内部实现。
  • 是否暴露第三方依赖类型。
  • 是否会造成循环依赖。

十二、常见误区

  1. paths 配好后认为 Node 自动支持别名。
  2. 混淆模块目标和模块解析策略。
  3. 通过一个 declare module 把第三方库整体变成 any
  4. 迁移时一次开启所有严格选项,导致项目失控。
  5. 长期保留 @ts-ignore,没有债务清单。
  6. 声明文件与真实 JavaScript 实现不同步。
  7. 只按技术层分目录,领域边界持续互相引用。
  8. 库发布只验证自己能构建,不验证消费端导入。

十三、实践练习

  1. 将单文件项目拆成 model.tsservice.tsmain.ts
  2. 配置路径别名,并分别为 TypeScript、测试工具和运行环境配置解析。
  3. 给一个无类型 JavaScript 函数编写 .d.ts
  4. allowJs + checkJs 检查一个 JS 模块。
  5. 搜索项目中的 @ts-ignore,改为 @ts-expect-error 或消除根因。

十四、总结

  1. 现代 TypeScript 项目优先使用 ES 模块。
  2. tsconfig 同时决定文件集合、检查规则、模块解析和产物。
  3. 模块配置必须与 Node、bundler 和 package.json 一致。
  4. paths 不是运行时别名实现。
  5. .d.ts 是对运行时事实的声明,错误声明同样危险。
  6. JavaScript 迁移应渐进推进,并持续缩小 any 与抑制指令。

请继续阅读:第7章:高级类型。


原始资料引用


  • 第5章:集合、异步与错误边界
  • 第7章:高级类型

本章目录
一、ES 模块是默认选择二、默认导出还是命名导出三、理解 tsconfig 的层次四、模块解析必须匹配运行环境五、路径映射不是运行时能力六、命名空间与模块七、声明文件 .d.ts八、为无类型模块补声明九、从 JavaScript 渐进迁移十、抑制错误的工具十一、边界设计十二、常见误区十三、实践练习十四、总结原始资料引用Related Documents
苏ICP备2025204887号-2