WinddSnow

TypeScript-Declaration-Files-and-Type-Declarations

字数统计: 4.3k阅读时长: 17 min
2026/07/31

第73课:声明文件与类型声明——.d.ts@types、全局声明、模块扩展

TypeScript 的强大依赖于类型信息。当你在 TypeScript 项目中使用 JavaScript 库(如 Lodash、Express)、浏览器 API(如 fetchlocalStorage)、或注入全局变量时,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
2
3
4
5
6
7
8
9
10
11
// 声明一个全局变量
declare var globalConfig: {
apiUrl: string;
debug: boolean;
};

// 声明常量(不可重新赋值)
declare const VERSION: string;

// 声明可变的 let 变量
declare let currentUser: { name: string } | null;

语法规则

  • .d.ts 文件中,declare 关键字可以省略(因为声明文件中的所有内容自动被视为声明)。
  • .ts 普通文件中,必须使用 declare 明确标识这是外部声明而非实现。
  • declare vardeclare letdeclare const 的区别与 JavaScript 一致:const 声明只读,let 可重新赋值。

2.2 声明函数

仅描述函数的参数类型和返回值类型,不包含函数体。

1
2
3
4
5
declare function fetchData(url: string, options?: RequestInit): Promise<Response>;

// 函数重载
declare function createId(): string;
declare function createId(prefix: string): string;

2.3 声明类

描述类的构造函数、实例属性和方法,不包含实现。

1
2
3
4
5
declare class EventEmitter {
on(event: string, listener: (...args: any[]) => void): this;
off(event: string, listener: (...args: any[]) => void): this;
emit(event: string, ...args: any[]): boolean;
}

2.4 声明命名空间(Namespace)

命名空间用于组织那些暴露在全局的嵌套对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare namespace MyLib {
function init(config: Config): void;

interface Config {
baseUrl: string;
timeout?: number;
}

namespace Utils {
function formatDate(date: Date): string;
}
}

// 使用
MyLib.init({ baseUrl: '/' });

注意:现代 TypeScript 推荐使用 ES Modules 而非 namespacenamespace 主要用于描述遗留全局库或内部模块),但理解它对于阅读 DefinitelyTyped 的类型定义仍然是必要的。

2.5 声明枚举

1
2
3
4
5
6
declare enum HttpStatusCode {
OK = 200,
Created = 201,
NotFound = 404,
InternalServerError = 500
}

3. 全局声明与模块声明

声明文件的行为取决于它所在的脚本环境——是全局脚本,还是模块。

3.1 全局声明文件

如果 .d.ts 文件不包含任何 importexport 语句,它被视为全局脚本,其中声明的类型将直接暴露在全局作用域中。这是描述浏览器全局变量(如 window.myPlugin)或通过 <script> 标签引入的库的典型方式。

1
2
3
4
5
6
7
8
9
// types/global.d.ts —— 注意:无任何 import/export

interface Window {
myPlugin: {
track(event: string, data?: Record<string, unknown>): void;
};
}

declare const APP_VERSION: string;

使用此声明后,在项目的任何地方都可以直接访问 window.myPluginAPP_VERSION,无需显式导入。

3.2 模块声明文件

如果 .d.ts 文件包含 export 语句,它被视为模块,其中的类型必须通过 import 导入才能使用。

1
2
3
4
5
// types/my-lib.d.ts
export function hello(name: string): string;
export interface Options {
debug: boolean;
}
1
2
// 使用方
import { hello, Options } from 'my-lib';

3.3 环境模块声明:declare module

当需要为一个没有类型定义的第三方 npm 包提供类型时,使用 declare module 语法。它告诉编译器:“如果一个模块被导入且路径匹配这个字符串,就使用这个类型定义”。

1
2
3
4
5
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
export function doSomething(input: string): number;
export const VERSION: string;
}
1
2
// 现在可以在代码中导入并使用
import { doSomething } from 'legacy-lib'; // ✅ 有类型

通配符模块声明:用于处理非 JavaScript 资源的导入(如 .css.svg.png 等),这在 Webpack/Vite 等打包工具项目中非常常见。

1
2
3
4
5
6
7
8
9
10
// types/static-assets.d.ts
declare module '*.svg' {
const content: string;
export default content;
}

declare module '*.module.css' {
const classes: Record<string, string>;
export default classes;
}

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 编写的第三方库(如 lodashexpress),该库本身没有自带类型声明文件。
  • 你使用的库已经自带类型(package.json 中有 "types": "./index.d.ts"),则无需额外安装 @types
1
2
3
4
5
// 库自带类型定义的示例
{
"name": "my-typed-lib",
"types": "./dist/index.d.ts"
}

4.3 查找类型的优先级

当 TypeScript 遇到 import { something } from 'some-lib' 时,它会按以下顺序查找类型定义:

  1. node_modules/some-lib/package.jsontypestypings 字段指向的文件。
  2. node_modules/some-lib/index.d.ts
  3. node_modules/@types/some-lib/index.d.ts(如果存在)。

如果以上都不存在,且模块没有类型声明,TypeScript 会根据 moduleResolution 配置报错或推断为 any(取决于 noImplicitAny)。


5. 扩展全局类型:declare global

在模块文件(即包含 export 的文件)中,不能直接向全局作用域添加类型。如果需要扩展全局接口(如 WindowString),必须使用 declare global 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// types/window-extension.d.ts
export {}; // 使文件成为模块(必须有至少一个 export)

declare global {
interface Window {
__INITIAL_STATE__: Record<string, unknown>;
analytics: {
track: (event: string) => void;
};
}

// 扩展 String 原型上的方法(如 polyfill)
interface String {
padStart(maxLength: number, fillString?: string): string;
}
}

关键规则

  • declare global 必须出现在模块文件中(即文件中有 importexport)。
  • 如果文件是脚本(无 import/export),则不需要 declare global 包裹,直接写接口即可(因为已经是全局作用域)。
  • 许多现代项目通过在 tsconfig.json 中配置 include 来引入全局类型文件(如 src/types/**/*.d.ts),无需手动导入。

6. 三斜线指令与类型引用

三斜线指令(Triple-Slash Directives)是 TypeScript 早期的模块引用机制,在现代项目中几乎已被 ESM 替代,但仍在某些场景中使用。

1
2
3
4
5
6
7
8
// 引用另一个声明文件
/// <reference path="./other.d.ts" />

// 声明依赖某个 @types 包(自动加载对应的类型)
/// <reference types="node" />

// 声明依赖某个库(自动加载对应的 @types 包)
/// <reference lib="es2022" />

现代替代

  • reference path → 被 tsconfig.jsoninclude 或 ESM import 替代。
  • reference types → 被 tsconfig.jsontypes 字段或直接 import 替代。
  • reference lib → 被 tsconfig.jsonlib 字段替代。

仅在以下场景三斜线指令仍有价值:

  • 全局脚本型声明文件声明依赖(因为这些文件没有 import,无法使用 ESM 引用)。
  • 特定工具链(如某些测试框架的全局类型注入)强制要求。

7. 实战:为现有 JavaScript 项目编写声明文件

7.1 示例:为一个简单的全局库编写声明

假设你在项目中通过 <script> 标签引入了一个全局库 MyWidget,它暴露了 window.MyWidget,需要为它编写类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// types/my-widget.d.ts(全局脚本,无 import/export)

interface MyWidgetOptions {
color?: string;
size?: 'small' | 'medium' | 'large';
onReady?: () => void;
}

declare class MyWidget {
constructor(element: HTMLElement, options?: MyWidgetOptions);
refresh(): void;
destroy(): void;
getValue(): string;
}

// 扩展 Window 接口
interface Window {
MyWidget: typeof MyWidget;
}

现在可以直接在 TypeScript 代码中使用:

1
2
3
4
5
6
7
8
const widget = new window.MyWidget(document.getElementById('app')!, {
color: 'blue',
size: 'large',
onReady: () => console.log('Widget ready!')
});

widget.refresh();
const val: string = widget.getValue();

7.2 示例:为 CommonJS 模块编写声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// types/my-utils.d.ts

declare module 'my-utils' {
export function formatCurrency(amount: number, currency: string): string;
export function parseDate(dateStr: string): Date;

export interface LogOptions {
level: 'debug' | 'info' | 'warn' | 'error';
timestamp: boolean;
}

export function createLogger(options: LogOptions): {
log: (message: string) => void;
};
}

8. 最佳实践与常见陷阱

8.1 声明文件的组织

  • 将应用范围的全局声明放在 src/types/ 目录下,通过 tsconfig.jsoninclude 引入。
  • 为每个第三方库或功能模块创建独立的 .d.ts 文件,用文件名表明其作用(如 global.d.tssvg.d.tsmy-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 行为不一致,就会出现“类型正确但运行时抛错”的危险情况。在编写声明文件时,务必对照实际源码或文档进行验证。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 声明文件的扩展名是什么?
    A. .type
    B. .d.ts
    C. .decl
    D. .tsx

  2. (单选) 以下关于 declare global 的描述,哪项是正确的?
    A. 它只能在非模块的全局脚本文件中使用。
    B. 它必须在模块文件(包含 importexport)中使用。
    C. 它用于声明局部变量。
    D. 它不需要任何条件即可使用。

  3. (填空) 要为 lodash 安装社区提供的类型定义,应执行 npm install -D ______

  4. (多选) 以下哪些是声明文件的作用?
    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(): voidupdateData(newData: number[]): voiddestroy(): void
  • ChartLib 添加到 Window 接口上,使 TypeScript 识别 window.ChartLib
  • 文件应作为全局脚本(无 import/export),以便在项目中直接生效。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 .d.ts 声明文件。提示词中需明确指定类的结构、配置接口的定义、以及 Window 接口的扩展方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为一个 React + TypeScript 项目创建全局类型声明文件 src/types/chartlib.d.ts,描述一个通过 CDN 引入的全局 JavaScript 图表库 ChartLib。需要:

  1. 定义接口 ChartConfig,包含 type: 'bar' | 'line'data: number[]width?: numberheight?: number
  2. 声明类 ChartLib,构造函数签名 constructor(element: HTMLElement, config: ChartConfig),实例方法 render(): voidupdateData(newData: number[]): voiddestroy(): void
  3. 扩展 Window 接口:interface Window { ChartLib: typeof ChartLib; }
  4. 文件不要包含 import/export(保持为全局脚本声明),添加 JSDoc 注释说明库的用途。
  5. 文件末尾添加一个简短的注释,说明如何在组件中使用(如 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、监控脚本),需要为其提供类型。
  • 需要扩展或覆盖第三方库的类型(如给 expressRequest 添加自定义属性)。
  • 需要为非 JavaScript 资源导入(如 .svg.css.vue 等)提供类型,以满足 TypeScript 模块解析。
  • 项目中的遗留 JavaScript 模块暂时无法迁移到 TypeScript,但希望获得部分类型检查。

3. 为 JavaScript 库编写类型声明的步骤

  1. 确定库的使用方式:它是全局脚本(通过 <script> 引入)还是 npm 模块(require/import)?
  2. 创建声明文件:在项目中创建 .d.ts 文件(如 types/my-lib.d.ts)。
  3. 描述 API 形状:使用 declare 关键字声明变量、函数、类、命名空间,或使用 declare module 描述模块导出。
  4. 添加接口和类型别名:为复杂对象参数定义清晰的接口。
  5. 声明全局变量:如果库暴露全局变量,通过 interface Window { ... } 扩展。
  6. 引入声明文件:通过 tsconfig.jsoninclude 字段引入,确保 TypeScript 能够加载它们。

课后练习答案

一、概念自测答案

  1. B

    • 解析:TypeScript 声明文件的扩展名是 .d.ts。A、C、D 不是标准扩展名。
  2. B

    • 解析:declare global 必须在模块文件(包含 importexport)中使用。如果文件是全局脚本,则无需 declare global 包裹,直接声明即为全局。
  3. @types/lodash

    • 解析:DefinitelyTyped 社区的类型定义通过 npm 的 @types/库名 形式发布,例如 @types/lodash
  4. A、B、C

    • 解析:声明文件可以为 JS 库提供类型(A)、为全局变量提供类型(B)、为静态资源提供类型(C)。它不生成 JavaScript 代码(D 错误)。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请生成一个 TypeScript 声明文件 chartlib.d.ts,为全局 JavaScript 图表库 ChartLib 提供类型。要求:

  • 定义 ChartConfig 接口:type: 'bar' | 'line'data: number[]width?: numberheight?: number
  • 声明 ChartLib 类:构造函数接收 element: HTMLElementconfig: ChartConfig;实例方法 render(): voidupdateData(newData: number[]): voiddestroy(): void
  • 扩展 Window 接口,添加 ChartLib: typeof ChartLib
  • 文件不包含 import/export(全局脚本声明),添加 JSDoc 注释。直接输出完整文件。”
CATALOG
  1. 1. 第73课:声明文件与类型声明——.d.ts、@types、全局声明、模块扩展
    1. 1.1. 1. 什么是声明文件?
      1. 1.1.1. 1.1 声明文件与普通 .ts 文件的区别
      2. 1.1.2. 1.2 声明文件的来源
    2. 1.2. 2. 声明语法:描述运行时的类型形状
      1. 1.2.1. 2.1 声明变量与常量
      2. 1.2.2. 2.2 声明函数
      3. 1.2.3. 2.3 声明类
      4. 1.2.4. 2.4 声明命名空间(Namespace)
      5. 1.2.5. 2.5 声明枚举
    3. 1.3. 3. 全局声明与模块声明
      1. 1.3.1. 3.1 全局声明文件
      2. 1.3.2. 3.2 模块声明文件
      3. 1.3.3. 3.3 环境模块声明:declare module
    4. 1.4. 4. @types 包与 DefinitelyTyped
      1. 1.4.1. 4.1 什么是 @types ?
      2. 1.4.2. 4.2 何时需要安装 @types?
      3. 1.4.3. 4.3 查找类型的优先级
    5. 1.5. 5. 扩展全局类型:declare global
    6. 1.6. 6. 三斜线指令与类型引用
    7. 1.7. 7. 实战:为现有 JavaScript 项目编写声明文件
      1. 1.7.1. 7.1 示例:为一个简单的全局库编写声明
      2. 1.7.2. 7.2 示例:为 CommonJS 模块编写声明
    8. 1.8. 8. 最佳实践与常见陷阱
      1. 1.8.1. 8.1 声明文件的组织
      2. 1.8.2. 8.2 避免声明与实现混合
      3. 1.8.3. 8.3 declare 的陷阱
      4. 1.8.4. 8.4 类型定义与运行时的一致性
    9. 1.9. 课后练习
      1. 1.9.1. 一、概念自测(选择题 / 填空题)
      2. 1.9.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.9.3. 三、Agent 模式下的提示词示例
      4. 1.9.4. 四、面试真题与参考答案
    10. 1.10. 课后练习答案
      1. 1.10.1. 一、概念自测答案
      2. 1.10.2. 二、AI 编程任务参考答案(提示词示例)