第73课:声明文件与类型声明——.d.ts、@types、全局声明、模块扩展
TypeScript 的强大依赖于类型信息。当你在 TypeScript 项目中使用 JavaScript 库(如 Lodash、Express)、浏览器 API(如 fetch、localStorage)、或注入全局变量时,TypeScript 编译器如何知道这些代码的类型?答案在于声明文件(.d.ts 文件)。声明文件是 TypeScript 生态的基石,它们为无类型的 JavaScript 代码赋予静态类型信息,使 IDE 能够提供智能提示、类型检查和重构支持。本节课将系统讲解声明文件的结构与语法、使用 DefinitelyTyped 社区的 @types 包、编写自己的全局声明和模块扩展,以及如何为第三方库贡献类型定义。
1. 什么是声明文件?
声明文件(Declaration File)以 .d.ts 为扩展名,只包含类型和接口的声明,不包含任何可执行代码。它们的作用是告诉 TypeScript 编译器:“在运行时,存在一个具有这些属性和方法的对象/模块/变量,请按照这个类型契约进行类型检查”。编译后,.d.ts 文件不会生成任何 JavaScript 代码。
1.1 声明文件与普通 .ts 文件的区别
| 特性 | 普通 .ts 文件 |
.d.ts 声明文件 |
|---|---|---|
| 内容 | 类型注解 + 可执行代码(函数体、变量值) | 仅类型声明,无任何实现 |
| 编译产物 | 生成 .js 文件 |
不生成任何 JavaScript 代码 |
export 行为 |
按模块导出 | 按模块导出类型声明,无实际值 |
| 作用 | 实现业务逻辑 | 描述已有 JavaScript 代码的类型形状 |
| 典型场景 | 应用源码 | 为库提供类型、描述全局变量、扩展已有类型 |
1.2 声明文件的来源
声明文件主要有三个来源:
- 编译自动生成:当
tsconfig.json中设置declaration: true时,编译器会为每个.ts源文件生成对应的.d.ts文件。这是库开发者的标准做法。 - 手动编写:为不存在类型的 JavaScript 代码(老旧库、内部工具)编写专用的声明文件。
- 社区贡献:从 DefinitelyTyped 仓库安装
@types/xxx包(如@types/lodash、@types/node)。
2. 声明语法:描述运行时的类型形状
声明文件中可以使用 declare 关键字来声明变量、函数、类、命名空间和模块。declare 告诉编译器:“这些实体在运行时已经存在,请信任我的类型描述”。
2.1 声明变量与常量
1 | // 声明一个全局变量 |
语法规则:
- 在
.d.ts文件中,declare关键字可以省略(因为声明文件中的所有内容自动被视为声明)。 - 在
.ts普通文件中,必须使用declare明确标识这是外部声明而非实现。 declare var、declare let、declare const的区别与 JavaScript 一致:const声明只读,let可重新赋值。
2.2 声明函数
仅描述函数的参数类型和返回值类型,不包含函数体。
1 | declare function fetchData(url: string, options?: RequestInit): Promise<Response>; |
2.3 声明类
描述类的构造函数、实例属性和方法,不包含实现。
1 | declare class EventEmitter { |
2.4 声明命名空间(Namespace)
命名空间用于组织那些暴露在全局的嵌套对象。
1 | declare namespace MyLib { |
注意:现代 TypeScript 推荐使用 ES Modules 而非 namespace(namespace 主要用于描述遗留全局库或内部模块),但理解它对于阅读 DefinitelyTyped 的类型定义仍然是必要的。
2.5 声明枚举
1 | declare enum HttpStatusCode { |
3. 全局声明与模块声明
声明文件的行为取决于它所在的脚本环境——是全局脚本,还是模块。
3.1 全局声明文件
如果 .d.ts 文件不包含任何 import 或 export 语句,它被视为全局脚本,其中声明的类型将直接暴露在全局作用域中。这是描述浏览器全局变量(如 window.myPlugin)或通过 <script> 标签引入的库的典型方式。
1 | // types/global.d.ts —— 注意:无任何 import/export |
使用此声明后,在项目的任何地方都可以直接访问 window.myPlugin 和 APP_VERSION,无需显式导入。
3.2 模块声明文件
如果 .d.ts 文件包含 export 语句,它被视为模块,其中的类型必须通过 import 导入才能使用。
1 | // types/my-lib.d.ts |
1 | // 使用方 |
3.3 环境模块声明:declare module
当需要为一个没有类型定义的第三方 npm 包提供类型时,使用 declare module 语法。它告诉编译器:“如果一个模块被导入且路径匹配这个字符串,就使用这个类型定义”。
1 | // types/legacy-lib.d.ts |
1 | // 现在可以在代码中导入并使用 |
通配符模块声明:用于处理非 JavaScript 资源的导入(如 .css、.svg、.png 等),这在 Webpack/Vite 等打包工具项目中非常常见。
1 | // types/static-assets.d.ts |
4. @types 包与 DefinitelyTyped
4.1 什么是 @types ?
DefinitelyTyped 是一个社区维护的类型定义仓库,包含数千个流行 JavaScript 库的类型定义。这些类型通过 npm 以 @types/库名 的形式发布。
1 | npm install -D @types/lodash @types/react @types/express |
安装后,TypeScript 编译器会自动从 node_modules/@types 中加载对应的类型声明。无需任何额外配置。
4.2 何时需要安装 @types?
- 你正在使用一个用 JavaScript 编写的第三方库(如
lodash、express),该库本身没有自带类型声明文件。 - 你使用的库已经自带类型(
package.json中有"types": "./index.d.ts"),则无需额外安装@types。
1 | // 库自带类型定义的示例 |
4.3 查找类型的优先级
当 TypeScript 遇到 import { something } from 'some-lib' 时,它会按以下顺序查找类型定义:
node_modules/some-lib/package.json的types或typings字段指向的文件。node_modules/some-lib/index.d.ts。node_modules/@types/some-lib/index.d.ts(如果存在)。
如果以上都不存在,且模块没有类型声明,TypeScript 会根据 moduleResolution 配置报错或推断为 any(取决于 noImplicitAny)。
5. 扩展全局类型:declare global
在模块文件(即包含 export 的文件)中,不能直接向全局作用域添加类型。如果需要扩展全局接口(如 Window、String),必须使用 declare global 包裹。
1 | // types/window-extension.d.ts |
关键规则:
declare global必须出现在模块文件中(即文件中有import或export)。- 如果文件是脚本(无
import/export),则不需要declare global包裹,直接写接口即可(因为已经是全局作用域)。 - 许多现代项目通过在
tsconfig.json中配置include来引入全局类型文件(如src/types/**/*.d.ts),无需手动导入。
6. 三斜线指令与类型引用
三斜线指令(Triple-Slash Directives)是 TypeScript 早期的模块引用机制,在现代项目中几乎已被 ESM 替代,但仍在某些场景中使用。
1 | // 引用另一个声明文件 |
现代替代:
reference path→ 被tsconfig.json的include或 ESMimport替代。reference types→ 被tsconfig.json的types字段或直接import替代。reference lib→ 被tsconfig.json的lib字段替代。
仅在以下场景三斜线指令仍有价值:
- 为全局脚本型声明文件声明依赖(因为这些文件没有
import,无法使用 ESM 引用)。 - 特定工具链(如某些测试框架的全局类型注入)强制要求。
7. 实战:为现有 JavaScript 项目编写声明文件
7.1 示例:为一个简单的全局库编写声明
假设你在项目中通过 <script> 标签引入了一个全局库 MyWidget,它暴露了 window.MyWidget,需要为它编写类型。
1 | // types/my-widget.d.ts(全局脚本,无 import/export) |
现在可以直接在 TypeScript 代码中使用:
1 | const widget = new window.MyWidget(document.getElementById('app')!, { |
7.2 示例:为 CommonJS 模块编写声明
1 | // types/my-utils.d.ts |
8. 最佳实践与常见陷阱
8.1 声明文件的组织
- 将应用范围的全局声明放在
src/types/目录下,通过tsconfig.json的include引入。 - 为每个第三方库或功能模块创建独立的
.d.ts文件,用文件名表明其作用(如global.d.ts、svg.d.ts、my-legacy-lib.d.ts)。 - 避免在单个
.d.ts中堆砌所有声明——这会导致维护困难和类型冲突。
8.2 避免声明与实现混合
.d.ts文件只应包含类型声明,绝对不要包含可执行代码(如函数体、变量赋值)。- 如果在普通
.ts文件中使用declare,确保该实体确实在运行时由外部提供,而非在当前文件中定义。
8.3 declare 的陷阱
declare class描述的是一个构造函数,而declare interface描述的是一个对象形状。混淆两者会导致类型错误。- 使用
declare var声明变量后,TypeScript 不会检查该变量是否真的在运行时存在。如果拼写错误或变量名不存在,类型检查通过但运行时崩溃——编写声明时要确保与运行时实现一一对应。
8.4 类型定义与运行时的一致性
声明文件是对运行时实际的承诺。如果声明文件描述的类型与真实 JavaScript 行为不一致,就会出现“类型正确但运行时抛错”的危险情况。在编写声明文件时,务必对照实际源码或文档进行验证。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 声明文件的扩展名是什么?
A..type
B..d.ts
C..decl
D..tsx(单选) 以下关于
declare global的描述,哪项是正确的?
A. 它只能在非模块的全局脚本文件中使用。
B. 它必须在模块文件(包含import或export)中使用。
C. 它用于声明局部变量。
D. 它不需要任何条件即可使用。(填空) 要为
lodash安装社区提供的类型定义,应执行npm install -D ______。(多选) 以下哪些是声明文件的作用?
A. 为 JavaScript 库提供类型信息。
B. 为全局变量(如window.myPlugin)提供类型。
C. 为导入非 JS 资源(如.svg、.css)提供类型。
D. 替代所有.ts文件生成 JavaScript 代码。
二、AI 编程任务:编写面向 AI 的提示词
场景:你正在一个 React + TypeScript 项目中集成一个第三方 JavaScript 图表库 chartlib(假设已通过 CDN 全局引入)。该库暴露了全局变量 ChartLib。你需要为它编写一个声明文件。要求如下:
- 声明
ChartLib全局变量,类型为一个类。 - 该类的构造函数接收一个 DOM 元素和一个配置对象
{ type: 'bar' | 'line'; data: number[]; width?: number; height?: number }。 - 该类拥有实例方法
render(): void、updateData(newData: number[]): void、destroy(): void。 - 将
ChartLib添加到Window接口上,使 TypeScript 识别window.ChartLib。 - 文件应作为全局脚本(无
import/export),以便在项目中直接生效。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 .d.ts 声明文件。提示词中需明确指定类的结构、配置接口的定义、以及 Window 接口的扩展方式。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请为一个 React + TypeScript 项目创建全局类型声明文件
src/types/chartlib.d.ts,描述一个通过 CDN 引入的全局 JavaScript 图表库ChartLib。需要:
- 定义接口
ChartConfig,包含type: 'bar' | 'line'、data: number[]、width?: number、height?: number。- 声明类
ChartLib,构造函数签名constructor(element: HTMLElement, config: ChartConfig),实例方法render(): void、updateData(newData: number[]): void、destroy(): void。- 扩展
Window接口:interface Window { ChartLib: typeof ChartLib; }。- 文件不要包含
import/export(保持为全局脚本声明),添加 JSDoc 注释说明库的用途。- 文件末尾添加一个简短的注释,说明如何在组件中使用(如
const chart = new window.ChartLib(el, config))。
确保代码符合 TypeScript 严格模式。完成后输出完整文件内容。
四、面试真题与参考答案
题目(蚂蚁集团前端面试题):
请解释 TypeScript 声明文件(
.d.ts)的作用。在什么情况下需要手动编写声明文件?如何为一个已存在的 JavaScript 库编写类型声明?请概述步骤。
参考答案:
1. 声明文件的作用:
声明文件(.d.ts)为 TypeScript 编译器提供类型信息,描述 JavaScript 代码运行时的结构——包括全局变量、函数、类、模块接口等。它们不包含任何可执行代码,仅在编译时进行类型检查,确保开发者以类型安全的方式使用 JavaScript 库或 API。
2. 需要手动编写声明文件的场景:
- 使用的第三方 JavaScript 库没有自带的类型定义,且社区
@types中也不存在对应的包。 - 项目中通过 CDN 或
<script>标签引入了全局变量(如埋点 SDK、监控脚本),需要为其提供类型。 - 需要扩展或覆盖第三方库的类型(如给
express的Request添加自定义属性)。 - 需要为非 JavaScript 资源导入(如
.svg、.css、.vue等)提供类型,以满足 TypeScript 模块解析。 - 项目中的遗留 JavaScript 模块暂时无法迁移到 TypeScript,但希望获得部分类型检查。
3. 为 JavaScript 库编写类型声明的步骤:
- 确定库的使用方式:它是全局脚本(通过
<script>引入)还是 npm 模块(require/import)? - 创建声明文件:在项目中创建
.d.ts文件(如types/my-lib.d.ts)。 - 描述 API 形状:使用
declare关键字声明变量、函数、类、命名空间,或使用declare module描述模块导出。 - 添加接口和类型别名:为复杂对象参数定义清晰的接口。
- 声明全局变量:如果库暴露全局变量,通过
interface Window { ... }扩展。 - 引入声明文件:通过
tsconfig.json的include字段引入,确保 TypeScript 能够加载它们。
课后练习答案
一、概念自测答案
B
- 解析:TypeScript 声明文件的扩展名是
.d.ts。A、C、D 不是标准扩展名。
- 解析:TypeScript 声明文件的扩展名是
B
- 解析:
declare global必须在模块文件(包含import或export)中使用。如果文件是全局脚本,则无需declare global包裹,直接声明即为全局。
- 解析:
@types/lodash- 解析:DefinitelyTyped 社区的类型定义通过 npm 的
@types/库名形式发布,例如@types/lodash。
- 解析:DefinitelyTyped 社区的类型定义通过 npm 的
A、B、C
- 解析:声明文件可以为 JS 库提供类型(A)、为全局变量提供类型(B)、为静态资源提供类型(C)。它不生成 JavaScript 代码(D 错误)。
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请生成一个 TypeScript 声明文件chartlib.d.ts,为全局 JavaScript 图表库ChartLib提供类型。要求:
- 定义
ChartConfig接口:type: 'bar' | 'line'、data: number[]、width?: number、height?: number。- 声明
ChartLib类:构造函数接收element: HTMLElement和config: ChartConfig;实例方法render(): void、updateData(newData: number[]): void、destroy(): void。- 扩展
Window接口,添加ChartLib: typeof ChartLib。- 文件不包含
import/export(全局脚本声明),添加 JSDoc 注释。直接输出完整文件。”