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

第13章:声明文件 —— 类型、值、命名空间与库发布

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

第13章:声明文件 —— 类型、值、命名空间与库发布

.d.ts 不是“把 JavaScript 大概翻译成接口”,而是对真实运行时 API 的精确建模。本章覆盖库结构识别、类型/值/命名空间、声明合并、模块增强、模板选择和 npm 发布。


一、声明文件只描述,不实现

ts
// math.d.ts
export function add(left: number, right: number): number;

声明中的函数没有实现体。消费端的 JavaScript 必须真实提供对应能力,否则类型检查通过也会在运行时失败。

二、同一个名字可能存在于不同空间

TypeScript 中声明可能创建:

  • 类型:接口、类型别名、类的实例侧。
  • :变量、函数、类、枚举。
  • 命名空间:可通过点号访问的类型/值容器。

类同时创建类型和值:

ts
class User {}
let user: User = new User();

接口只创建类型,不能在运行时访问。

三、先识别库的运行结构

编写声明前先看 JavaScript 如何被消费。

ES/CommonJS 模块库

ts
import { parse } from "library";

默认导出或 export =

ts
import Client from "library";
// 或 const Client = require("library")

全局库

html
<script src="library.js"></script>
js
Library.doWork();

UMD

既支持模块导入,又能作为全局变量使用。不同结构对应不同 .d.ts 模板,不能只凭 API 名字猜。

四、常见模块声明

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

  export function createClient(options: Options): Client;

  export interface Client {
    request(path: string): Promise<unknown>;
  }
}

模块内部的普通声明通常已经是局部的,按需要 export

五、可调用对象与属性

JavaScript 函数也可以带属性:

js
function build(options) {}
build.version = "1.0";

声明可组合函数与命名空间:

ts
declare function build(options: build.Options): void;

declare namespace build {
  interface Options {
    debug?: boolean;
  }
  const version: string;
}

export = build;

这就是函数声明与命名空间声明合并。

六、类模块和插件模块

类导出需要同时描述构造签名、实例成员和静态成员。插件模块可能修改另一个模块,应使用模块增强:

ts
import "http-client";

declare module "http-client" {
  interface Client {
    retry(count: number): this;
  }
}

模块增强只能补充现有声明,不能凭空创造与运行时不一致的实现。

七、声明合并

接口同名声明会合并:

ts
interface WindowConfig {
  apiUrl: string;
}

interface WindowConfig {
  debug: boolean;
}

合并后同时拥有两个属性。常见组合还包括:

  • 命名空间 + 类。
  • 命名空间 + 函数。
  • 命名空间 + 枚举。

类型别名不能声明合并。声明合并很强,但全局扩展应克制,避免多个库互相污染。

八、全局增强

在模块文件中扩展全局:

ts
export {};

declare global {
  interface Window {
    appVersion: string;
  }
}

必须确保运行时确实写入 window.appVersion。全局增强适合平台补充和框架注入,不适合作为普通模块通信方式。

九、声明文件的 Do 与 Don't

使用原始类型

使用 numberstringbooleansymbol,不要使用包装对象 NumberStringBooleanSymbol

不要无故使用 any

使用泛型保存关系,使用 unknown 表示需要检查的数据。

回调参数不要随意设为可选

库调用回调时如果总会提供参数,就应声明为必选;可选参数表示实现可能不给,含义不同。

重载优先可选参数或联合

能用一个可选参数或联合类型准确表达时,不要堆叠冗余重载。

返回 void 的回调

回调返回类型为 void 通常表示调用方忽略返回值,不代表实现绝对不能返回值。

十、从 JavaScript 生成声明

可配置 TypeScript 从带 JSDoc 的 JavaScript 生成 .d.ts

json
{
  "compilerOptions": {
    "allowJs": true,
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "types"
  }
}

生成结果仍需人工审查:JSDoc 是否完整、公共 API 是否意外暴露、泛型关系是否丢失。

十一、发布声明

最佳方式通常是将声明与 npm 包一起发布:

json
{
  "types": "./dist/index.d.ts",
  "files": ["dist"]
}

注意:

  • 声明依赖应放在正确的 dependencies 中。
  • 避免在发布声明中使用脆弱的 /// <reference path>
  • typesVersions 可为不同 TypeScript 版本提供声明,但增加维护成本。
  • 无法随包发布时,可贡献到 @types

十二、声明消费测试

至少建立:

text
fixture-esm/
fixture-cjs/
fixture-bundler/

测试内容:

  • 导入语法是否可用。
  • 自动补全是否符合预期。
  • 错误调用是否被拒绝。
  • 声明是否引用了未发布的内部路径。
  • 运行时导出与类型导出是否一致。

十三、常见误区

  1. 先写 .d.ts,后看 JavaScript 实现。
  2. 把模块整体声明为 any 后长期不修。
  3. 用接口描述运行时需要 instanceof 的实体。
  4. 回调参数全部写成可选。
  5. 重载顺序从宽泛到具体,导致具体签名不可达。
  6. 全局增强没有对应运行时初始化。
  7. 声明文件引用源码内部路径,发布后丢失。
  8. 只检查声明能生成,不测试消费端。

十四、练习

  1. 为一个函数对象编写“可调用 + 属性”的声明。
  2. 分别为模块库、全局库和 UMD 库选择声明模板。
  3. 给既有模块添加一个真实存在的插件方法声明。
  4. 从 JSDoc JavaScript 生成 .d.ts 并审查。
  5. 建立最小 ESM 消费项目测试声明。

十五、总结

  1. .d.ts 必须忠实描述运行时 JavaScript。
  2. 类型、值和命名空间是理解复杂声明的基础。
  3. 库结构决定声明模板。
  4. 声明合并和模块增强用于描述 JavaScript 的动态组合能力。
  5. 声明发布必须配合真实消费测试。

请继续阅读:第14章:JavaScript 渐进类型化。


原始资料引用


  • 第12章:模块深潜
  • 第14章:JavaScript 渐进类型化

本章目录
一、声明文件只描述,不实现二、同一个名字可能存在于不同空间三、先识别库的运行结构四、常见模块声明五、可调用对象与属性六、类模块和插件模块七、声明合并八、全局增强九、声明文件的 Do 与 Don't十、从 JavaScript 生成声明十一、发布声明十二、声明消费测试十三、常见误区十四、练习十五、总结原始资料引用Related Documents
苏ICP备2025204887号-2