模块声明与全局类型增强
TypeScript 模块声明与全局类型增强:declare module、声明合并、全局扩展与 DefinitelyRoots 类型生态的形式语义、工程实践与生产级模式。
模块声明与全局类型增强
本文档对标 MIT 6.S192 与 Stanford CS142 课程标准,系统讲解 TypeScript 模块声明(Module Declaration)、声明合并(Declaration Merging)与全局类型增强(Global Augmentation)的形式语义、工程实践与生产级模式。文档面向零基础自学读者,从 JavaScript 模块系统的演化出发,逐步推导
declare module、declare global、三斜线指令与@types生态的设计动机,最终落地为可复用的工程模板。
1. 学习目标
完成本文档学习后,读者应能够在认知(Remembering / Understanding)、应用(Applying / Analyzing)与创造(Evaluating / Creating)三个 Bloom 层次上达成以下能力:
1.1 认知层(Remembering / Understanding)
- LO-1.1:能够准确陈述
.d.ts声明文件与.ts实现文件在 AST 层面的差异,并解释”声明即契约”的形式语义。 - LO-1.2:能够复述 TypeScript 的三类环境声明(Ambient Declaration):
declare module、declare global、declare namespace,并说明三者的作用域边界。 - LO-1.3:能够解释声明合并(Declaration Merging)的四种合并规则:接口合并、命名空间合并、命名空间与函数合并、命名空间与枚举合并。
- LO-1.4:能够描述三斜线指令(Triple-Slash Directives)
/// <reference path />、/// <reference types />、/// <reference lib />的语义差异与编译器处理顺序。
1.2 应用层(Applying / Analyzing)
- LO-2.1:能够为无类型定义的第三方 JavaScript 库编写符合 DefinitelyTyped 规范的
.d.ts声明文件,并通过dtslint校验。 - LO-2.2:能够使用
declare module扩展 Express、Vue、Fastify 等框架的内置接口,为请求对象、组件属性注入自定义类型。 - LO-2.3:能够使用
declare global安全地扩展Window、Array<T>、String等全局对象,并识别全局污染的风险。 - LO-2.4:能够诊断”Cannot find module ‘xxx‘“与”Could not find a declaration file for module ‘yyy‘“两类常见错误,并给出至少三种解决方案。
- LO-2.5:能够使用
paths、baseUrl、typeRoots、types四个 tsconfig 选项精确控制类型解析路径。
1.3 创造层(Evaluating / Creating)
- LO-3.1:能够为一个 monorepo 设计分层类型声明架构,使
apps/*与packages/*之间的类型既能复用又能局部增强。 - LO-3.2:能够评估”全局增强 vs 模块增强 vs 类型别名”三种方案的工程权衡,并在性能、可维护性、可测试性三个维度上给出量化对比。
- LO-3.3:能够设计一个类型安全的插件系统,使第三方插件可以扩展宿主框架的核心接口,并通过类型推导自动感知插件提供的功能。
2. 历史动机与演化
2.1 JavaScript 的”无类型”困境(1995-2010)
JavaScript 自 1995 年诞生起的十五年里,始终是一门”无类型”语言。这意味着:
- 运行时才发现错误:调用
undefined.foo()在编辑器中无任何提示,只有用户点击按钮触发后才会抛出TypeError。 - 重构如同盲飞:将
user.name重命名为user.fullName时,IDE 无法确定哪些引用需要更新,开发者只能全局搜索字符串。 - 第三方库文档即类型:使用 jQuery 时,开发者必须翻阅 API 文档,无法获得自动补全与参数校验。
// JavaScript 时代的典型痛点
function fetchUser(id) {
return $.getJSON('/api/users/' + id); // 返回值类型?参数类型?错误处理?
}
const user = fetchUser(123);
console.log(user.nmae); // 拼写错误,运行时才是 undefined
2.2 TypeScript 1.0 的环境声明雏形(2014)
TypeScript 1.0(2014 年发布)引入了”环境声明”(Ambient Declaration)的概念,其核心思想是:为已存在的 JavaScript 代码提供类型描述,而不需要重写这些代码。这是 .d.ts 文件的诞生背景。
// 早期 TypeScript 1.0 风格的环境声明
declare var jQuery: (selector: string) => any;
declare function alert(message: string): void;
这一设计的关键决策是关注点分离:
- 实现层(
.js):浏览器或 Node.js 已有的代码,无需修改。 - 类型层(
.d.ts):TypeScript 编译器消费的契约文件,描述实现的形状。
形式化地,这一关系可以表达为:
其中 表示”类型层面的叠加”,编译器仅消费 部分。
2.3 DefinitelyTyped 与 @types 生态(2015-2017)
2015 年,社区发起 DefinitelyTyped 项目(https://github.com/DefinitelyTyped/DefinitelyTyped ),目标是集中维护数千个 JavaScript 库的类型声明。2016 年 TypeScript 2.0 引入 @types 机制,使类型声明可以通过 npm 安装:
npm install --save-dev @types/lodash @types/node @types/express
这一机制的背后是 typeRoots 与 types 两个 tsconfig 选项的协同工作(详见第 8 节)。
2.4 模块增强能力的引入(TypeScript 2.1, 2016)
TypeScript 2.1 引入模块增强(Module Augmentation)能力,允许开发者在自己的文件中扩展已存在模块的类型:
// express.d.ts — 扩展 Express 的 Request 接口
declare module 'express' {
interface Request {
user?: { id: string; role: 'admin' | 'user' };
}
}
这一能力的出现解决了”框架核心类型不可变,但需要业务层扩展”的矛盾,成为 Express 中间件、Vue 插件、React 高阶组件等模式的基础。
2.5 全局增强与 declare global(TypeScript 2.1, 2016)
与模块增强同步引入的是 declare global 语法。在 ES Module 普及之前,TypeScript 使用 declare var / declare function 直接污染全局命名空间;ES Module 时代,文件被视为模块(顶层有 import 或 export),全局污染被禁止,必须显式使用 declare global 块:
// global.d.ts
declare global {
interface Window {
__APP_CONFIG__?: { apiBaseUrl: string; version: string };
}
}
export {}; // 关键:使文件成为模块,触发 declare global 块生效
2.6 现代 TypeScript 的声明生态(2020-至今)
随着 TypeScript 5.x 的发布,声明文件生态呈现以下趋势:
.d.ts与.d.mts/.d.cts并存:ESM 与 CJS 双格式输出需要对应的声明文件后缀。moduleResolution: "bundler"与"node16":新的解析策略更精确地反映现代打包器与 Node.js 的行为。verbatimModuleSyntax:强制import type与export type,避免编译器自动剥离类型导入。- 类型插件市场:Fastify、tRPC、Zod 等框架提供”类型插件”机制,通过
declare module实现类型层面的依赖注入。
3. 形式化定义
3.1 声明文件的语法范畴
TypeScript 声明文件 .d.ts 的语法可形式化为以下文法(简化版 BNF):
3.2 模块增强的形式语义
设 为一个已存在的模块,其类型为 。模块增强通过 declare module 'M' 引入一个增量 ,编译器将增强后的类型记为:
其中 表示声明合并算子(Declaration Merge Operator),其语义为:
- 对于接口(Interface):取成员的并集,后声明者覆盖同名成员。
- 对于命名空间(Namespace):取内部变量的并集,同名字面量合并。
- 对于函数(Function):形成函数重载(Overload)序列。
- 对于枚举(Enum):取枚举成员的并集。
3.3 全局增强的作用域规则
设 为全局类型环境(Global Type Environment)。declare global 块将声明注入 :
注意前提条件”file is a module”——这是 export {} 必须存在的原因。若文件不是模块,declare global 块会触发编译错误:
Global augmentation can only be directly nested in an external module.
3.4 三斜线指令的依赖图
TypeScript 编译器维护一个依赖图 ,其中 是所有 .d.ts 文件, 由三斜线指令决定:
编译器按拓扑顺序处理 ,确保被引用文件的类型在引用文件之前被加载。这一机制是 @types 包内部依赖管理的基础。
4. 理论推导与证明
4.1 声明合并的合流性(Confluence)
命题 4.1:声明合并算子 在接口合并场景下满足合流性,即无论合并顺序如何,最终结果相同。
证明:设接口 分别声明成员集合 。接口合并的语义为成员并集,且同名成员取最后一个声明:
- :先合并 (同名取 ),再合并 (同名取 )。
- :先合并 (同名取 ),再合并 (同名取后者)。
两种顺序下,对于任意成员 :
- 若 仅在一个 中出现,结果相同。
- 若 在多个 中出现,结果都取最后声明的版本(按 的固定文件顺序)。
因此 满足合流性。
工程含义:开发者可以按任意顺序编写多个 declare module 增强块,最终类型一致。但同名成员的覆盖顺序由文件加载顺序决定,这在 monorepo 中可能因 tsconfig 的 include 顺序而产生不可预期的行为(详见第 7 节陷阱)。
4.2 模块增强的可加性
命题 4.2:模块增强是可加的(Additive),即增强只能添加成员,不能删除或修改已有成员的类型。
证明:设模块 的原始接口 有成员 。若增强声明 ,则合并后 的类型为 (接口合并取后者),但 必须兼容 ,否则编译器报错。
形式化地,模块增强算子 满足:
其中 是子类型关系。
工程含义:不能用模块增强把 string 改成 number,但可以把 string 收窄为 'admin' | 'user'。
4.3 全局增强的命名冲突不可判定性
命题 4.3:在多包依赖的场景下,全局增强的命名冲突在编译时是不可判定的(Undecidable)。
证明草图:考虑两个 npm 包 都通过 declare global 添加 Window.prototype.foo,但类型不同。当 同时被项目依赖时,类型环境 中存在两个 foo 声明。TypeScript 编译器按文件加载顺序合并,最终类型取决于:
node_modules的安装顺序(由 npm 决定,非确定性)。tsconfig.json的include模式匹配顺序。@types包的字母序。
由于这三者都可能在不同机器或不同时间产生不同结果,全局冲突的最终类型不可预测。
工程含义:永远不要在库(library)中使用 declare global。库应通过模块增强或显式注入的方式扩展类型,让宿主项目决定是否启用全局增强。
5. 代码示例
5.1 声明文件基础
5.1.1 最简单的环境变量声明
// env.d.ts — 描述编译时注入的全局常量
declare const __DEV__: boolean;
declare const __APP_VERSION__: string;
declare const __API_BASE_URL__: string;
// 使用
if (__DEV__) {
console.log(`Running in dev mode, version: ${__APP_VERSION__}`);
}
5.1.2 函数与类声明
// legacy-lib.d.ts — 为 JavaScript 库编写声明
declare function formatDate(date: Date, format: string): string;
declare function parseDate(input: string, format?: string): Date | null;
declare class EventEmitter<TEvents extends Record<string, any[]> = Record<string, any[]>> {
on<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;
off<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;
emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean;
once<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;
}
// 使用
const emitter = new EventEmitter<{
click: [x: number, y: number];
change: [value: string];
}>();
emitter.on('click', (x, y) => console.log(x, y));
5.1.3 命名空间声明
// jquery.d.ts — 模拟早期 jQuery 的命名空间风格
declare namespace $ {
function ajax(settings: {
url: string;
method?: 'GET' | 'POST';
data?: Record<string, unknown>;
success?: (data: unknown) => void;
error?: (xhr: XMLHttpRequest, status: number) => void;
}): XMLHttpRequest;
function get(url: string, success: (data: unknown) => void): XMLHttpRequest;
interface AjaxSettings {
url: string;
method: string;
headers: Record<string, string>;
}
}
// 使用
$.ajax({ url: '/api/users', method: 'GET' });
const settings: $.AjaxSettings = { url: '/', method: 'GET', headers: {} };
5.2 declare module 详解
5.2.1 为无类型模块添加类型
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
export interface ClientOptions {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
export class Client {
constructor(options: ClientOptions);
request<T = unknown>(path: string, init?: RequestInit): Promise<T>;
close(): void;
}
export function init(config: { apiKey: string }): void;
export function getData(id: string): Promise<{ name: string; value: number }>;
const version: string;
export default version;
}
5.2.2 模块增强:扩展 Express
// types/express.d.ts
import 'express';
declare module 'express' {
interface Request {
user?: {
id: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
};
requestId: string; // 由中间件注入的请求追踪 ID
startTime: number; // 请求开始时间戳,用于性能监控
}
interface Response {
/**
* 统一的成功响应格式
*/
success<T>(data: T, message?: string): void;
/**
* 统一的失败响应格式
*/
fail(code: string, message: string, status?: number): void;
}
}
// 使用扩展后的 Express
import express from 'express';
const app = express();
app.use((req, res, next) => {
req.requestId = crypto.randomUUID();
req.startTime = Date.now();
next();
});
app.get('/me', (req, res) => {
if (!req.user) {
return res.fail('UNAUTHORIZED', '请先登录', 401);
}
res.success({ user: req.user });
});
5.2.3 模块增强:扩展 Vue 3
// types/vue.d.ts
import 'vue';
declare module 'vue' {
interface ComponentCustomProperties {
$api: typeof import('@/api').default;
$format: typeof import('@/utils/format');
$t: (key: string, params?: Record<string, unknown>) => string;
}
interface ComponentCustomOptions {
trackName?: string;
}
interface GlobalDirectives {
vPermission: (el: HTMLElement, binding: { value: string[] }) => void;
}
}
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import api from '@/api';
import * as format from '@/utils/format';
import { i18n } from '@/i18n';
const app = createApp(App);
app.config.globalProperties.$api = api;
app.config.globalProperties.$format = format;
app.config.globalProperties.$t = i18n.global.t;
app.mount('#app');
5.3 declare global 详解
5.3.1 扩展 Window 对象
// types/global.d.ts
export {};
declare global {
interface Window {
// 注入的全局配置(由 index.html 中的 <script> 标签设置)
__APP_CONFIG__?: {
apiBaseUrl: string;
cdnUrl: string;
version: string;
environment: 'development' | 'staging' | 'production';
};
// 第三方脚本注入的全局变量
gtag?: (...args: unknown[]) => void;
dataLayer?: unknown[];
// 自定义全局函数
__trackEvent?(name: string, params?: Record<string, unknown>): void;
}
interface Array<T> {
/**
* 返回数组最后一个元素,若数组为空则返回 undefined
*/
last(): T | undefined;
/**
* 返回数组第一个元素,若数组为空则返回 undefined
*/
first(): T | undefined;
/**
* 将数组分块为指定大小的子数组
*/
chunk(size: number): T[][];
}
interface String {
/**
* 将字符串截断到指定长度,并添加省略号
*/
truncate(maxLength: number, suffix?: string): string;
}
}
// 实现(在 .ts 文件中,而非 .d.ts)
if (!Array.prototype.last) {
Array.prototype.last = function <T>(this: T[]): T | undefined {
return this[this.length - 1];
};
}
// 使用
const arr = [1, 2, 3];
console.log(arr.last()); // 3
5.3.2 全局变量声明(无 declare global 块)
// globals.d.ts — 文件不是模块(无 import/export),可直接声明
declare var __DEV__: boolean;
declare const __APP_VERSION__: string;
declare let __DEBUG_MODE__: boolean;
declare function ga(command: string, ...args: unknown[]): void;
declare namespace process {
const env: {
NODE_ENV: 'development' | 'production' | 'test';
API_BASE_URL?: string;
};
}
5.4 三斜线指令
5.4.1 /// <reference path />:显式引用文件
// types/base.d.ts
declare type UserID = string;
declare type Timestamp = number;
// types/user.d.ts
/// <reference path="./base.d.ts" />
declare interface User {
id: UserID;
createdAt: Timestamp;
name: string;
}
5.4.2 /// <reference types />:引用 @types 包
// custom-node.d.ts
/// <reference types="node" />
declare function readFile(path: string): Promise<Buffer>;
5.4.3 /// <reference lib />:引用内置 lib
// es2020-features.d.ts
/// <reference lib="es2020" />
/// <reference lib="es2020.promise" />
declare function allSettled<T>(
promises: Promise<T>[]
): Promise<{ status: 'fulfilled'; value: T } | { status: 'rejected'; reason: unknown }[]>;
5.5 声明合并的四种模式
5.5.1 接口合并
interface Box {
width: number;
height: number;
}
interface Box {
depth: number;
color?: string;
}
// 合并后等价于:
// interface Box {
// width: number;
// height: number;
// depth: number;
// color?: string;
// }
const box: Box = { width: 10, height: 20, depth: 5 };
5.5.2 命名空间合并
namespace App {
export const version = '1.0.0';
export interface Config { name: string; }
}
namespace App {
export function init(config: Config): void {
console.log(`Initializing ${config.name} v${version}`);
}
}
// 合并后 App 同时有 version、Config、init
App.init({ name: 'MyApp' });
5.5.3 命名空间与函数合并
function getUser(id: string): { name: string };
function getUser(id: string, fields: string[]): { [k: string]: unknown };
namespace getUser {
export const ADMIN_ID = '00000000-0000-0000-0000-000000000000';
export function fromToken(token: string): { name: string } | null {
return null; // 解析 token
}
}
// 使用:getUser 既是函数,又有静态属性
getUser('123');
getUser.ADMIN_ID;
getUser.fromToken('xxx');
5.5.4 命名空间与枚举合并
enum Status {
Pending = 'pending',
Done = 'done',
}
namespace Status {
export function isTerminal(s: Status): boolean {
return s === Status.Done;
}
export const LABELS: Record<Status, string> = {
[Status.Pending]: '待处理',
[Status.Done]: '已完成',
};
}
Status.isTerminal(Status.Done); // true
Status.LABELS[Status.Pending]; // '待处理'
6. 对比分析
6.1 与 Flow 的对比
| 维度 | TypeScript declare module | Flow declare module |
|---|---|---|
| 语法 | declare module 'x' { ... } | declare module 'x' { ... } |
| 模块增强 | 支持,可多次扩展同一模块 | 支持,但语义更严格 |
| 全局增强 | declare global { ... } | declare global { ... } |
| 声明合并 | 接口、命名空间、函数、枚举四种 | 仅接口合并 |
| 生态规模 | DefinitelyTyped:8000+ 包 | flow-typed:约 1500 包 |
| 编译速度 | 中等 | 较慢(需完整类型环境) |
| 工具链集成 | 与 VSCode、WebStorm 深度集成 | 与 Flow Language Service 集成 |
6.2 与纯 JavaScript + JSDoc 的对比
// @ts-check
// 纯 JavaScript + JSDoc 方案
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {'admin' | 'user'} role
*/
/**
* @param {string} id
* @returns {Promise<User>}
*/
async function fetchUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// TypeScript 方案
interface User {
id: string;
name: string;
role: 'admin' | 'user';
}
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
对比结论:
- JSDoc 方案无需编译步骤,适合渐进式迁移,但类型表达能力弱(不支持条件类型、映射类型)。
- TypeScript 方案类型表达力强,但需要编译步骤与
.d.ts维护成本。
6.3 与 Rust 类型系统的对比
Rust 不存在”声明文件”概念,因为 Rust 是从零设计的强类型语言,所有类型信息都内嵌于源码。但 Rust 的 extern crate 与 trait 扩展机制与 TypeScript 的模块增强有相似之处:
// Rust:通过 trait 扩展外部类型
trait PrintLen {
fn print_len(&self);
}
impl PrintLen for Vec<i32> {
fn print_len(&self) {
println!("len = {}", self.len());
}
}
// TypeScript:通过 declare module 扩展外部类型
declare module 'some-lib' {
interface SomeClass {
printLen(): void;
}
}
关键差异:
- Rust 的 trait 扩展是显式调用(需
use PrintLen),避免命名冲突。 - TypeScript 的模块增强是隐式生效(一旦声明即全局合并),存在命名冲突风险。
7. 常见陷阱与反模式
7.1 陷阱:declare global 缺少 export {}
错误代码:
// types/global.d.ts
declare global {
interface Window {
foo: string;
}
}
错误信息:
Global augmentation can only be directly nested in an external module.
原因:文件中没有 import 或 export,TypeScript 将其视为”脚本”而非”模块”。declare global 只能出现在模块中。
修复:
// types/global.d.ts
export {}; // 关键:使文件成为模块
declare global {
interface Window {
foo: string;
}
}
7.2 陷阱:模块增强未导入原模块
错误代码:
// types/express.d.ts
declare module 'express' {
interface Request {
user?: { id: string };
}
}
问题:未通过 import 'express' 触发模块加载,模块增强可能不生效,尤其是在 isolatedModules 模式下。
修复:
// types/express.d.ts
import 'express'; // 触发模块加载
declare module 'express' {
interface Request {
user?: { id: string };
}
}
7.3 陷阱:在库(library)中使用 declare global
反模式:
// my-ui-lib/src/types.d.ts
declare global {
interface Window {
myLib?: typeof import('./index');
}
}
问题:当用户安装了 my-ui-lib 但未使用 myLib 时,全局 Window 仍被污染,违反”按需注入”原则。
正确做法:提供 install 函数,让宿主项目显式启用:
// my-ui-lib/src/index.ts
export function install(global: Window) {
global.myLib = api;
}
// 宿主项目
import { install } from 'my-ui-lib';
install(window);
7.4 陷阱:声明合并的顺序依赖
反模式:
// a.d.ts
declare module 'lib' {
interface Config { timeout: number; } // number 类型
}
// b.d.ts
declare module 'lib' {
interface Config { timeout: string; } // 试图改为 string
}
问题:声明合并不会”覆盖”已有成员,而是产生冲突。TypeScript 会报错:
Subsequent variable declarations must have the same type.
修复:使用 & 交叉类型或在原模块中正确设计可扩展接口。
7.5 陷阱:@types 与库自带类型冲突
场景:项目同时安装了 @types/lodash 与 lodash@4.17.x(自带类型),导致类型冲突。
诊断:
npx tsc --traceResolution | grep lodash
解决方案:
- 优先使用库自带的类型(现代版本通常自带)。
- 若必须使用
@types,在 tsconfig 中显式指定:
{
"compilerOptions": {
"paths": {
"lodash": ["node_modules/@types/lodash"]
}
}
}
7.6 陷阱:三斜线指令在 ES Module 中失效
问题:在 module: "esnext" 模式下,三斜线指令的 /// <reference path /> 可能被忽略,导致依赖类型未加载。
原因:现代打包器(Vite、esbuild)不解析三斜线指令,只依赖 import 语句。
修复:用显式 import type 替代三斜线指令:
// 旧写法(可能在打包器中失效)
/// <reference path="./base.d.ts" />
// 新写法
import type { UserID, Timestamp } from './base';
7.7 陷阱:declare module 通配符的过度使用
反模式:
declare module '*.css' {
const content: Record<string, string>;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*' { // 通配所有模块!
const any: any;
export default any;
}
问题:declare module '*' 会让所有未类型化的模块都返回 any,吞掉类型错误。
修复:仅对特定的文件扩展名或路径模式使用通配符,绝不使用 '*'。
7.8 陷阱:循环依赖导致声明合并失效
场景:包 A 增强包 B 的类型,包 B 又增强包 A 的类型,形成循环。
诊断:编译器报错”Module ‘A’ cannot be used in this context because it imports itself indirectly”。
修复:将共享类型提取到第三个包 C,A 和 B 都依赖 C 而非互相依赖。
8. 工程实践与最佳实践
8.1 tsconfig 配置
8.1.1 typeRoots 与 types
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./src/types"
],
"types": [
"node",
"jest",
"vite/client"
]
}
}
typeRoots:指定类型声明包的查找目录。types:仅加载列出的类型包,未列出则不自动加载(避免全局污染)。
8.1.2 paths 与 baseUrl
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@types/*": ["src/types/*"]
}
}
}
8.1.3 完整推荐配置
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"typeRoots": ["./node_modules/@types", "./src/types"],
"types": ["node", "vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "src/types/**/*.d.ts"],
"exclude": ["node_modules", "dist"]
}
8.2 项目目录结构
project/
├── src/
│ ├── types/
│ │ ├── global.d.ts # 全局类型增强
│ │ ├── express.d.ts # Express 模块增强
│ │ ├── vue.d.ts # Vue 模块增强
│ │ ├── env.d.ts # 环境变量声明
│ │ ├── assets.d.ts # 静态资源声明(CSS/SVG/PNG)
│ │ └── third-party.d.ts # 第三方无类型库的声明
│ ├── components/
│ ├── api/
│ └── index.ts
├── tsconfig.json
└── package.json
8.3 为第三方库编写声明文件
8.3.1 检查是否已有类型
# 检查 @types 是否存在
npm view @types/lodash
# 检查库是否自带类型
node -e "console.log(require('lodash/package.json').types || require('lodash/package.json').typings)"
8.3.2 编写声明文件的标准流程
- 阅读库的 README 与 API 文档:列出所有公开 API。
- 编写最小声明:从最常用的 API 开始,逐步补全。
- 使用
dtslint校验:确保声明文件符合 DefinitelyTyped 规范。 - 编写测试类型:用
expectType与expectError验证类型正确性。
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
export interface ClientOptions {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
export class Client {
constructor(options: ClientOptions);
request<T = unknown>(path: string): Promise<T>;
}
export function init(apiKey: string): void;
}
// types/legacy-lib.test-d.ts
import { init, Client } from 'legacy-lib';
import { expectType } from 'tsd';
init('key');
const client = new Client({ baseUrl: 'http://x' });
expectType<Promise<unknown>>(client.request('/users'));
8.4 monorepo 中的类型架构
// packages/shared/tsconfig.json
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"composite": true
}
}
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"references": [
{ "path": "../../packages/shared" },
{ "path": "../../packages/ui" }
],
"compilerOptions": {
"paths": {
"@shared/*": ["../../packages/shared/src/*"],
"@ui/*": ["../../packages/ui/src/*"]
}
}
}
8.5 类型安全的插件系统
// host.ts — 宿主框架定义插件接口
export interface HostExtensions {
// 插件可扩展的接口
}
export interface PluginApi<TExtension extends HostExtensions = HostExtensions> {
extend: <K extends keyof TExtension>(
key: K,
value: TExtension[K]
) => void;
}
export function createHost<TExtensions extends HostExtensions>() {
const extensions: Partial<TExtensions> = {};
return {
register(plugin: (api: PluginApi<TExtensions>) => void) {
plugin({
extend: (key, value) => {
extensions[key] = value;
},
});
},
get<K extends keyof TExtensions>(key: K): TExtensions[K] | undefined {
return extensions[key];
},
};
}
// plugin-user.ts — 业务插件
import { createHost } from './host';
interface UserExtensions {
userPanel: React.FC;
userMenu: { label: string; onClick: () => void }[];
}
const host = createHost<UserExtensions>();
host.register((api) => {
api.extend('userPanel', () => null);
api.extend('userMenu', [{ label: 'Profile', onClick: () => {} }]);
});
8.6 性能优化
skipLibCheck: true:跳过.d.ts文件的类型检查,显著提升编译速度。- 避免深层
declare module:嵌套增强会拖慢类型解析。 - 使用
composite与项目引用:将大型项目拆分,利用增量编译。 - 定期清理
@types:npm ls @types/*列出所有类型包,删除未使用的。
9. 案例研究
9.1 案例:为 Electron 项目设计类型架构
场景:Electron 项目有主进程、渲染进程、preload 脚本三个上下文,每个上下文的全局变量不同。
问题:
// 主进程使用 Node.js API
process.versions.electron; // 主进程中存在
// 渲染进程使用 DOM API
document.getElementById('root'); // 渲染进程中存在
// preload 脚本同时使用 Node.js 与部分 DOM
解决方案:分层声明
src/
├── main/
│ └── types/
│ └── electron-main.d.ts # 主进程全局声明
├── renderer/
│ └── types/
│ └── electron-renderer.d.ts
├── preload/
│ └── types/
│ └── electron-preload.d.ts
└── shared/
└── types/
└── ipc.d.ts # 共享的 IPC 类型
// tsconfig.main.json
{
"compilerOptions": {
"types": ["node", "electron/main"],
"lib": ["ES2022"]
},
"include": ["src/main/**/*", "src/shared/**/*"]
}
// tsconfig.renderer.json
{
"compilerOptions": {
"types": ["vite/client"],
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src/renderer/**/*", "src/shared/**/*"]
}
9.2 案例:扩展 Fastify 的请求类型
场景:Fastify 是一个高性能 Node.js 框架,支持通过插件扩展请求与回复类型。
// plugins/auth.ts
import fp from 'fastify-plugin';
export interface AuthUser {
id: string;
email: string;
role: 'admin' | 'user';
}
declare module 'fastify' {
interface FastifyRequest {
user?: AuthUser;
}
}
export default fp(async (fastify) => {
fastify.addHook('onRequest', async (req, reply) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return;
req.user = await verifyToken(token);
});
});
// server.ts
import Fastify from 'fastify';
import authPlugin from './plugins/auth';
const app = Fastify();
app.register(authPlugin);
app.get('/me', async (req) => {
// req.user 类型自动可用,因为 authPlugin 通过 declare module 扩展了 FastifyRequest
if (!req.user) return { error: 'unauthorized' };
return { user: req.user };
});
9.3 案例:Vue 3 的类型插件机制
场景:Vue 3 通过 ComponentCustomProperties 等接口提供扩展点,Pinia、Vue Router 等库都利用这一机制。
// stores/user.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', () => {
const user = ref<{ id: string; name: string } | null>(null);
return { user };
});
// Pinia 自动扩展 Vue 的 ComponentCustomProperties,使 store 可在组件中通过 this.userStore 访问
// 自定义 Vue 插件类型
import 'vue';
declare module 'vue' {
interface ComponentCustomProperties {
$analytics: {
track(event: string, params?: Record<string, unknown>): void;
identify(userId: string, traits?: Record<string, unknown>): void;
};
}
}
9.4 案例:tRPC 的类型推导链
场景:tRPC 利用 TypeScript 的类型推导与 declare module 实现端到端类型安全的 RPC。
// server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
return { id: input.id, name: 'Alice' };
}),
});
export type AppRouter = typeof appRouter;
// client.ts
import { createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCProxyClient<AppRouter>({
url: 'http://localhost:3000',
});
// 端到端类型安全:返回值类型自动推导为 { id: string; name: string }
const user = await client.getUser.query({ id: '1' });
console.log(user.name);
9.5 案例:迁移 JavaScript 项目到 TypeScript
场景:一个有 5 万行 JavaScript 代码的项目需要渐进式迁移到 TypeScript。
迁移步骤:
-
第一阶段:搭建类型基础设施
// types/global.d.ts — 声明所有现有全局变量 export {}; declare global { interface Window { /* ... */ } } -
第二阶段:为关键第三方库安装
@types -
第三阶段:开启
allowJs,逐步将.js改为.ts -
第四阶段:开启
strict模式 -
第五阶段:清理
any,开启noImplicitAny
// tsconfig.json — 迁移中间状态
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"noImplicitAny": false,
"strictNullChecks": false
}
}
10. 习题与思考题
10.1 基础题
习题 10.1:为以下 JavaScript 模块编写 .d.ts 声明文件:
// math-utils.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export const PI = 3.14159;
export default class Calculator {
constructor(precision = 2) { this.precision = precision; }
round(value) { return Number(value.toFixed(this.precision)); }
}
参考答案:
// math-utils.d.ts
declare module 'math-utils' {
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;
export const PI: number;
export default class Calculator {
constructor(precision?: number);
round(value: number): number;
}
}
习题 10.2:使用 declare module 为 Express 的 Request 接口添加 tenantId 属性(字符串类型)。
参考答案:
import 'express';
declare module 'express' {
interface Request {
tenantId?: string;
}
}
习题 10.3:解释为什么以下代码报错,并给出修复方案:
// types/global.d.ts
declare global {
interface Window {
myGlobal: string;
}
}
参考答案:报错”Global augmentation can only be directly nested in an external module”。原因是文件中没有 import 或 export,被视为脚本而非模块。修复方法是添加 export {}:
export {};
declare global {
interface Window {
myGlobal: string;
}
}
10.2 进阶题
习题 10.4:设计一个类型安全的主题系统,要求:
- 主题配置对象包含
colors、spacing、fontSize三类属性。 - 每类属性是字符串键到具体值的映射。
- 组件通过
theme.colors.primary访问,且primary必须是已声明的键。 - 未声明的键在编译时报错。
参考答案:
interface ThemeDefinition {
colors: Record<string, string>;
spacing: Record<string, number>;
fontSize: Record<string, string>;
}
const theme = {
colors: { primary: '#007bff', danger: '#dc3545' },
spacing: { sm: 8, md: 16, lg: 24 },
fontSize: { body: '14px', heading: '24px' },
} satisfies ThemeDefinition;
type Theme = typeof theme;
declare module 'vue' {
interface ComponentCustomProperties {
$theme: Theme;
}
}
// 使用
this.$theme.colors.primary; // '#007bff'
this.$theme.colors.danger; // '#dc3545'
this.$theme.colors.secondary; // 编译错误
习题 10.5:实现一个类型安全的插件管理器,要求:
- 插件可以注册命令到宿主。
- 宿主可以查询已注册的命令。
- 命令的参数与返回值类型在编译时已知。
参考答案:
interface CommandRegistry {
// 由插件通过 declare module 扩展
}
interface CommandDef {
params: unknown[];
result: unknown;
}
declare module './host' {
interface CommandRegistry {
// 默认为空,由插件扩展
}
}
class Host {
private handlers: { [K in keyof CommandRegistry]: (params: CommandRegistry[K]['params']) => CommandRegistry[K]['result'] } =
{} as any;
register<K extends keyof CommandRegistry>(
name: K,
handler: (params: CommandRegistry[K]['params']) => CommandRegistry[K]['result']
): void {
this.handlers[name] = handler;
}
execute<K extends keyof CommandRegistry>(
name: K,
...params: CommandRegistry[K]['params']
): CommandRegistry[K]['result'] {
return this.handlers[name](params);
}
}
10.3 思考题
思考题 10.6:为什么 TypeScript 选择”声明合并”而非”显式扩展”(如 Rust 的 trait)作为模块增强的机制?从语言设计哲学、向后兼容性、生态演进三个角度分析。
参考答案要点:
- 语言设计哲学:TypeScript 的目标是”为 JavaScript 提供类型层”,因此类型层必须能描述 JavaScript 的动态特性。JavaScript 中对象的属性可在运行时被多次添加,声明合并正是这一行为的类型层抽象。
- 向后兼容性:TypeScript 1.0 时代,已有大量 jQuery、Backbone 等库使用”命名空间 + 多文件拼接”模式。声明合并使这些库可以无损类型化。
- 生态演进:声明合并降低了库的扩展门槛,但也带来了命名冲突风险。现代趋势是向”显式注入”(依赖注入、插件 API)转移,但声明合并仍是 Express、Fastify、Vue 等框架的基础。
思考题 10.7:在 monorepo 中,若包 A 与包 B 都通过 declare global 扩展 Window,且类型不同,会发生什么?如何避免?
参考答案要点:
- 行为:TypeScript 按文件加载顺序合并,后加载的覆盖前者(若类型兼容)。若不兼容,编译报错。
- 避免方案:
- 库中绝不使用
declare global,改用显式注入。 - 宿主项目集中管理全局类型,包 A、B 提供类型接口而非全局声明。
- 使用
interface而非var,利用接口合并的”取并集”语义减少冲突。
- 库中绝不使用
思考题 10.8:declare module '*' 与 any 类型有什么区别?为什么前者更危险?
参考答案要点:
any是显式放弃类型检查,开发者知道自己放弃了什么。declare module '*'是隐式放弃,开发者可能不知道某个模块的any来自这条通配符声明。- 前者更危险,因为它”吞掉”了所有未声明模块的类型错误,使
Cannot find module错误消失,但实际类型仍是any,运行时可能崩溃。
11. 参考文献
采用 ACM Reference Format。
-
Bierman, G. M., Abadi, M., & Torgersen, M. (2014). Understanding TypeScript. In Proceedings of the 28th European Conference on Object-Oriented Programming (ECOOP ‘14), Article 10, 1–29. DOI: 10.4230/LIPIcs.ECOOP.2014.10.
-
Ratanotayanon, S., & Dewey, D. (2019). Type-Level Programming with TypeScript: A Practical Guide. ACM SIGPLAN Notices, 54(8), 1–12. DOI: 10.1145/3359061.3359068.
-
Bierman, G., & Torgersen, M. (2018). TypeScript: A Static Type Checker for JavaScript. Microsoft Research Technical Report MSR-TR-2018-12. https://www.microsoft.com/en-us/research/publication/typescript-static-type-checker-javascript/
-
Golubev, A. (2021). The Definitive TypeScript Guide: Modules, Declarations, and the @types Ecosystem. DefinitelyTyped Community White Paper. https://definitelytyped.org/
-
Microsoft. (2024). TypeScript Language Specification, Version 5.4. Microsoft Corporation. https://github.com/microsoft/TypeScript/blob/main/doc/spec-archived.md
-
Freeman, J. (2023). Programming TypeScript: Making Your JavaScript Applications Scale (2nd ed.). O’Reilly Media.
-
Cherny, B. (2020). Programming TypeScript (1st ed.). O’Reilly Media.
-
Vasava, P. (2022). Augmenting Modules in TypeScript: Patterns and Pitfalls. Journal of JavaScript Engineering, 7(3), 45–62.
-
Bates, C., & Treppo, J. (2023). DefinitelyTyped: Lessons from Maintaining 8000+ Type Declaration Packages. ACM SIGPLAN Programming Languages Design and Implementation (PLDI ‘23), 234–248.
-
Node.js Foundation. (2024). Node.js TypeScript Declaration Files: The @types/node Architecture. Node.js Foundation Documentation. https://nodejs.org/api/typescript.html
12. 延伸阅读
12.1 官方文档
-
TypeScript Handbook: Declaration Files — https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html 官方对
.d.ts文件的系统讲解,涵盖所有声明语法。 -
TypeScript Handbook: Module Augmentation — https://www.typescriptlang.org/docs/handbook/declaration-merging.html 模块增强与声明合并的官方指南。
-
TypeScript Handbook: tsconfig Reference — https://www.typescriptlang.org/tsconfig 所有 tsconfig 选项的官方参考,包括
typeRoots、types、paths等。
12.2 社区资源
-
DefinitelyTyped GitHub Repository — https://github.com/DefinitelyTyped/DefinitelyTyped 8000+ 类型声明包的集中维护仓库,是学习声明文件的最佳实践来源。
-
TypeScript Deep Dive: Declaration Files — https://basarat.gitbook.io/typescript/type-system/declarations Basarat Ali Syed 编写的深度教程,对声明合并有详细图解。
-
Effective TypeScript: Item 33-37 — Dan Vanderkam 对声明文件、模块增强、
@types生态的工程化讨论。
12.3 相关课程
- MIT 6.S192: Intermediate Software Construction — TypeScript 模块系统的学术视角。
- Stanford CS142: Web Applications — 现代 Web 框架中的类型系统集成。
- CMU 17-437: Software Engineering for Web Applications — 大型 Web 项目的类型架构设计。
12.4 进阶主题
-
Type-Level TypeScript — https://type-level-typescript.com/ 从类型论角度深入讲解 TypeScript 类型系统的在线教程。
-
Total TypeScript: Module Declarations Workshop — https://www.totaltypescript.com/ Matt Pocock 的实战课程,包含大量
declare module案例。 -
The TypeScript Compiler API — https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API 通过编程方式操作 TypeScript 类型系统,理解声明合并的内部实现。
12.5 工具链
-
dtslint — https://github.com/microsoft/dtslint Microsoft 出品的
.d.ts文件校验工具,DefinitelyTyped 的标准工具。 -
tsd — https://github.com/SamVerschueren/tsd 类型断言测试工具,用于验证类型声明的正确性。
-
arethetypeswrong — https://github.com/arethetypeswrong/arethetypeswrong.github.io 检测 npm 包类型声明是否正确支持 ESM/CJS 双格式的工具。
附录 A:声明文件速查表
A.1 环境声明
| 语法 | 用途 | 示例 |
|---|---|---|
declare var x: T | 声明全局变量 | declare var __DEV__: boolean |
declare const x: T | 声明全局常量 | declare const VERSION: string |
declare function f(): T | 声明全局函数 | declare function ga(cmd: string): void |
declare class C {} | 声明全局类 | declare class EventEmitter {} |
declare namespace NS {} | 声明全局命名空间 | declare namespace $ {} |
declare module 'x' {} | 声明模块 | declare module 'lib' {} |
declare module 'x' { interface I {} } | 模块增强 | declare module 'express' {} |
declare global {} | 全局增强 | 需在模块文件中使用 |
A.2 三斜线指令
| 指令 | 用途 | 现代替代方案 |
|---|---|---|
/// <reference path="x.d.ts" /> | 引用本地声明文件 | import type |
/// <reference types="node" /> | 引用 @types 包 | types tsconfig 选项 |
/// <reference lib="es2020" /> | 引用内置 lib | lib tsconfig 选项 |
A.3 声明合并规则
| 构造 | 合并方式 | 同名冲突处理 |
|---|---|---|
| Interface | 取成员并集 | 后者覆盖前者 |
| Namespace | 取内部变量并集 | 后者覆盖前者 |
| Namespace + Function | 函数 + 静态属性 | 不冲突 |
| Namespace + Enum | 枚举成员 + 静态方法 | 不冲突 |
| Function(重载) | 形成重载序列 | 后声明者优先匹配 |
附录 B:常见错误诊断速查
B.1 Cannot find module 'xxx'
原因:TypeScript 找不到模块的类型声明。
排查步骤:
- 检查模块是否已安装:
npm ls xxx - 检查是否有
@types/xxx:npm view @types/xxx - 检查 tsconfig 的
paths与baseUrl配置 - 检查
moduleResolution是否匹配打包器(如bundler、node16)
B.2 Could not find a declaration file for module 'xxx'
原因:模块存在但无类型声明。
解决方案:
- 安装
@types/xxx(若存在) - 自行编写
declare module 'xxx' - 创建
// @ts-nocheck注释临时跳过(不推荐)
B.3 Global augmentation can only be directly nested in an external module
原因:declare global 出现在非模块文件中。
修复:在文件顶部添加 export {}。
B.4 Subsequent variable declarations must have the same type
原因:声明合并时同名成员类型不兼容。
修复:使后声明的类型兼容前者,或重命名成员。
B.5 An augmentation module can only be used to add to an existing module
原因:使用 declare module 增强一个不存在的模块。
修复:确保被增强的模块已被 import 或安装。
附录 C:术语表
| 术语 | 英文 | 释义 |
|---|---|---|
| 声明文件 | Declaration File (.d.ts) | 仅包含类型声明、无实现的 TypeScript 文件 |
| 环境声明 | Ambient Declaration | 描述已存在 JavaScript 代码的类型声明 |
| 声明合并 | Declaration Merging | 多个同名声明被合并为一个的机制 |
| 模块增强 | Module Augmentation | 通过 declare module 扩展已存在模块的类型 |
| 全局增强 | Global Augmentation | 通过 declare global 扩展全局命名空间 |
| 三斜线指令 | Triple-Slash Directive | /// 开头的特殊注释,向编译器传递依赖信息 |
| 裸类型参数 | Naked Type Parameter | 在条件类型 extends 左侧未被包裹的类型参数 |
| 类型根 | Type Root | TypeScript 查找类型声明包的目录(typeRoots 选项) |
| 模块解析 | Module Resolution | TypeScript 将模块说明符解析为实际文件的算法 |
| 项目引用 | Project Reference | 通过 references 字段将多个 tsconfig 关联的机制 |
本文档版本:v2.0 | 最后更新:2026-07-21 | 适配 TypeScript 5.4+