第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>
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
使用原始类型
使用 number、string、boolean、symbol,不要使用包装对象 Number、String、Boolean、Symbol。
不要无故使用 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/
测试内容:
- 导入语法是否可用。
- 自动补全是否符合预期。
- 错误调用是否被拒绝。
- 声明是否引用了未发布的内部路径。
- 运行时导出与类型导出是否一致。
十三、常见误区
- 先写 .d.ts,后看 JavaScript 实现。
- 把模块整体声明为 any 后长期不修。
- 用接口描述运行时需要 instanceof 的实体。
- 回调参数全部写成可选。
- 重载顺序从宽泛到具体,导致具体签名不可达。
- 全局增强没有对应运行时初始化。
- 声明文件引用源码内部路径,发布后丢失。
- 只检查声明能生成,不测试消费端。
十四、练习
- 为一个函数对象编写“可调用 + 属性”的声明。
- 分别为模块库、全局库和 UMD 库选择声明模板。
- 给既有模块添加一个真实存在的插件方法声明。
- 从 JSDoc JavaScript 生成 .d.ts 并审查。
- 建立最小 ESM 消费项目测试声明。
十五、总结
- .d.ts 必须忠实描述运行时 JavaScript。
- 类型、值和命名空间是理解复杂声明的基础。
- 库结构决定声明模板。
- 声明合并和模块增强用于描述 JavaScript 的动态组合能力。
- 声明发布必须配合真实消费测试。
请继续阅读:第14章:JavaScript 渐进类型化。
原始资料引用
- 第12章:模块深潜
- 第14章:JavaScript 渐进类型化