编写声明文件:全局库
本教程共 80 篇 · 第 62 篇 · 更新于 2026-08-10 · 约 14 分钟阅读
本节目标:学会为全局库编写完整的 .d.ts 声明文件。你会掌握命名空间的组织方式、如何用 interface/type 声明可复用类型、全局函数的多种写法,以及让项目识别全局声明的配置方法。
全局库长什么样
全局库(Global Library)是最早的 JS 库形态。它不依赖任何模块加载器,直接在全局作用域挂载一个对象,通过 <script> 标签引入就能用。
典型特征是:
- 在 HTML 里通过
<script src="...">引入 - 代码里直接通过全局变量名访问,比如
$(jQuery)、_(lodash 全局版) - 源码里通常有对
window或globalThis的赋值
举个例子,一个叫 MyLib 的全局库,JS 侧可能是这样写的:
// mylib.js —— 全局库的实现
window.MyLib = (function() {
var version = "2.0.0";
function createMessage(name) {
return "Hello, " + name + "!";
}
function showDialog(options) {
console.log("Dialog:", options.title);
}
return {
version: version,
createMessage: createMessage,
showDialog: showDialog
};
})();
使用方式极其简单——不用 require,不用 import:
<script src="mylib.js"></script>
<script>
MyLib.showDialog({ title: "Welcome" });
</script>
第一步:分析 API 结构
在动手写声明文件之前,先梳理清楚库暴露了哪些东西:
- 一个全局对象
MyLib MyLib.version:字符串,只读MyLib.createMessage(name):接收字符串,返回字符串MyLib.showDialog(options):接收一个对象,有title和可选的width、height
分析清楚了再写声明,事半功倍。很多人在这一步偷懒,结果声明文件写出来对不上实际 API,反而引入了更多 bug。
基本声明写法
把上面的 API 翻译成 TypeScript 声明:
// mylib.d.ts
declare namespace MyLib {
const version: string;
function createMessage(name: string): string;
function showDialog(options: {
title: string;
width?: number;
height?: number;
}): void;
}
这个文件放在项目里(比如 src/types/mylib.d.ts),然后在 tsconfig.json 的 include 里包含它,项目中就能直接用 MyLib.xxx 且获得完整的类型提示。
把参数类型内联在函数签名里写(options: { title: string; ... })只适合简单场景。参数一复杂,你就会想把它抽成独立类型。
用 interface 组织可复用类型
showDialog 的参数可以抽出为一个 interface:
// mylib.d.ts(改进版)
declare namespace MyLib {
const version: string;
function createMessage(name: string): string;
function showDialog(options: DialogOptions): void;
interface DialogOptions {
title: string;
width?: number;
height?: number;
}
}
抽成 interface 有三个好处:
- 可复用:如果多个函数用同样的参数结构,interface 只用写一次。
- 方便使用方引用:别人可以
const opts: MyLib.DialogOptions = { ... }把配置先定义好再传进去。 - 可扩展:用 interface 声明,以后还能通过声明合并(declaration merging)追加属性。
Tip放在 namespace 内的 interface 仍然是类型(不会被编译为运行时代码),但它的访问路径是
MyLib.DialogOptions,和 JS 侧的MyLib.showDialog自然地在同一个命名空间下。
当全局库本身可调用
有些全局库暴露的是一个函数而不是对象。比如一个叫 initApp 的库,它本身就是一个函数,同时上面还挂了几个属性:
// initApp 在运行时是一个函数,上面挂了 version 属性
function initApp(config) {
// ...
}
initApp.version = "3.0.0";
initApp.reset = function() { /* ... */ };
这种”可调用 + 有属性”的组合,在声明文件里要同时用 declare function 和 declare namespace:
// initapp.d.ts
declare function initApp(config: InitConfig): void;
declare namespace initApp {
const version: string;
function reset(): void;
interface InitConfig {
apiKey: string;
debug?: boolean;
}
}
为什么 function 和 namespace 同名可以共存?因为 TypeScript 允许同一个名字在不同上下文中承担不同角色:initApp 作为”值”是可调用函数,作为”命名空间”包含静态属性和类型。这叫声明合并,在 TS 的类型系统里是完全合法的。
函数重载的声明
全局函数经常需要支持多种不同参数组合。比如一个 log 函数,可以接收一个字符串或一个对象:
// logger.d.ts
declare function log(message: string): void;
declare function log(data: { level: string; message: string }): void;
重载声明的规则:
- 每个
declare function是一个独立的类型签名(重载) - 越具体的重载放在越前面(TS 按从上到下匹配第一个兼容的签名)
- 参数类型的兼容性判断是”第一个能匹配的就被选中”
正确的顺序示例:
// ✅ 正确:具体在前,通用在后
declare function log(data: { level: string; message: string }): void;
declare function log(message: string): void;
如果反过来把通用签名 log(message: string) 放前面,对象参数就会先匹配到 string 版本(TS 的重载解析只看兼容,不挑”最佳”),导致类型推断错误。
完整的全局库声明模板
把上面的知识组合起来,就是一个完整、可扩展的全局库声明文件:
// mylib.d.ts —— 全局库声明模板
declare namespace MyLib {
// 常量
const version: string;
// 函数
function createMessage(name: string): string;
function showDialog(options: DialogOptions): void;
// 类型
interface DialogOptions {
title: string;
width?: number;
height?: number;
}
type LogLevel = "debug" | "info" | "warn" | "error";
// 类(如果库有构造函数需要 new)
class Logger {
constructor(level: LogLevel);
log(message: string): void;
}
// 枚举
enum Status {
Idle,
Loading,
Success,
Error
}
}
几点说明:
- 所有类型(interface、type)放在 namespace 内部,避免污染全局。
- 命名结构反映了 JS 运行时的真实 API 结构——运行时怎么用,声明文件就怎么写。
- 类声明不需要
declare前缀,因为在 namespace 内部不是顶层。
让项目识别全局声明
写好声明文件后,你需要让 TS 编译器加载它。
方式一:放在 include 里
{
"compilerOptions": {
"strict": true,
"types": []
},
"include": ["src/**/*", "src/types/**/*.d.ts"]
}
把所有自定义的 .d.ts 统一放在 src/types/ 下,然后 include 整个目录。这是最直白的方式,适合声明文件跟着项目走的情况。
方式二:使用三斜线指令(不推荐新项目)
/// <reference path="./types/mylib.d.ts" />
这是早期 TypeScript 的做法,现在基本被 include/files 取代。只在一些遗留项目里能看到。
方式三:配置 typeRoots
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/types"]
}
}
不过 TS 7.0 中 types: [] 是默认值(不再自动加载所有 @types),所以 typeRoots 的使用场景变少了。推荐用 include。
一个常见坑:命名冲突
在全局声明文件中直接写顶层 interface 或 type(不放在 namespace 里)容易跟别的库冲突:
// ❌ 不好:顶层声明,容易和别的库冲突
interface DialogOptions {
title: string;
}
declare namespace MyLib {
function showDialog(options: DialogOptions): void;
}
如果一个叫 OtherLib 的声明文件也定义了叫 DialogOptions 的接口,两者就会冲突。正确的做法是把类型包在 namespace 里:
// ✅ 好:类型在 namespace 内,不会冲突
declare namespace MyLib {
interface DialogOptions {
title: string;
}
function showDialog(options: DialogOptions): void;
}
遵守一个简单原则:全局库声明文件的命名空间就是库暴露的全局变量名。在这个命名空间内部,类型名怎么取都行,不会干扰外部。
总结
写全局库声明文件的思路:
- 先分析 JS 库的 API 结构——它暴露了什么变量、函数、类。
- 用
declare namespace包裹所有声明,避免命名污染。 - 函数用
declare function,支持重载(具体在前)。 - 参数复杂时用 interface 抽出来,放在 namespace 内。
- 通过
tsconfig.json的include让 TS 加载声明文件。
全局库声明是声明文件家族里最基础的一种模式。下一章我们转向模块库——那些需要 import 才能用的库,声明文件的写法完全不同。