24.声明文件.d.ts 副本.md
TypeScript 的强大之处在于其静态类型系统,它能在我们编码时就提供无与伦比的智能提示、错误检查和代码导航。但这引出了一个关键问题:当我们的 TypeScript 项目需要使用一个纯 JavaScript 编写的库(如一个 jQuery 插件、一个旧版的 npm 包,或者一个内部的工具库)时,会发生什么?
对于 TypeScript 编译器来说,这个 JavaScript 库就像一个“黑箱”。它不知道这个库导出了哪些函数,这些函数需要什么参数,又会返回什么。因此,编译器只能无奈地将所有从该库导入的东西都视为 any 类型。这意味着,我们失去了所有类型安全保障,回到了“刀耕火种”的 JavaScript 时代。
为了解决这个问题,TypeScript 提供了一套解决方案,其核心就是声明文件 (Declaration Files),它们以 .d.ts 为后缀。
什么是声明文件?
你可以将 .d.ts 文件想象成一本API 使用手册。它只描述 JavaScript 代码的“形状”(shape),而不包含任何具体的实现。它告诉 TypeScript 编译器:
- 这个库里有哪些可用的变量、函数和类。
- 这些函数的参数是什么类型,返回值是什么类型。
- 这些类有哪些属性和方法。
- ...以及所有关于其公共 API 的类型信息。
它是一座桥梁,连接了 TypeScript 的静态类型世界和 JavaScript 的动态运行时世界。
在你动手写之前:先寻找现有的声明文件
在你准备为某个库从零开始编写 .d.ts 文件之前,请务必先做两件事,这能为你节省大量时间:
- 检查库本身:许多现代 JavaScript 库在发布时已经内置了自己的
.d.ts文件。这是一个越来越普遍的最佳实践。 - 搜索 DefinitelyTyped:DefinitelyTyped 是一个庞大的、由社区驱动的开源项目,它为数千个流行的 JavaScript 库提供了高质量的
.d.ts文件。这些文件都发布在 npm 的@typesscope 下。
例如,如果你想在项目中使用 lodash,你只需要运行:
npm install --save-dev @types/lodash
安装后,TypeScript 编译器会自动找到并使用这些类型定义,你就可以享受到对 lodash 的完整类型支持了。
只有在这两种方式都失败时,我们才需要亲手创建自己的声明文件。
动手实践:为一个 JS 模块创建声明文件
假设我们有一个简单的、内部使用的 JavaScript 工具库 string-utils.js:
// file: lib/string-utils.js
function padLeft(str, len, char) {
return char.repeat(len - str.length) + str;
}
function countChars(str) {
return str.length;
}
module.exports = {
padLeft,
countChars,
};
这是一个典型的 CommonJS 模块。现在,让我们在 TypeScript 项目中使用它:
// file: src/index.ts
import * as utils from '../lib/string-utils'; // TypeScript 不知道 utils 的类型,会将其视为 any
const result = utils.padLeft("hello", 10, " "); // 没有类型提示,容易出错
为了修复这个问题,我们需要创建一个 string-utils.d.ts 文件。
第 1 步:创建 .d.ts 文件
在你的项目中创建一个文件,例如 types/string-utils.d.ts。
第 2 步:使用 declare module
因为这是一个模块,我们需要使用 declare module '...' 语法。模块的名称必须与你在 import 语句中使用的路径完全匹配。
// file: types/string-utils.d.ts
declare module '../lib/string-utils' {
// 我们将在这里描述模块的 API
}
第 3 步:描述导出的成员
现在,在 declare module 块内部,我们像编写普通 TypeScript 代码一样,使用 export 来描述 string-utils.js 导出的内容。
// file: types/string-utils.d.ts
declare module '../lib/string-utils' {
/**
* Pads a string on the left.
* @param str The string to pad.
* @param len The total desired length.
* @param char The character to pad with.
*/
export function padLeft(str: string, len: number, char: string): string;
/**
* Counts the characters in a string.
* @param str The input string.
*/
export function countChars(str: string): number;
}
关键点:
- 我们只写了函数的签名,没有函数体
{}。 - 我们使用了
export关键字,因为原始 JS 模块导出了这些函数。 - 添加 JSDoc 注释是一个非常好的习惯,它能让使用者的体验更上一层楼。
第 4 步:让 TypeScript 找到它
最后,确保你的 tsconfig.json 能够找到你创建的声明文件。通常,include 配置会自动处理,或者你可以通过 typeRoots 明确指定。
现在,回到我们的 index.ts 文件,你会发现奇迹发生了:
// file: src/index.ts
import * as utils from '../lib/string-utils';
// 1. 获得完整的智能提示
// 2. 获得参数类型检查
const result = utils.padLeft("hello", 10, " ");
// utils.padLeft("hello", 10, 123);
// 编译时错误: Argument of type 'number' is not assignable to parameter of type 'string'.
声明全局变量
对于那些不通过模块系统、而是直接向全局作用域(如浏览器的 window 对象)添加变量的旧脚本,我们可以使用 declare var, declare let, 或 declare const。
例如,为一个通过 <script> 标签引入的 MY_GLOBAL_LIB 库创建声明:
// file: types/global.d.ts
interface LibConfig {
apiKey: string;
}
declare function MY_GLOBAL_LIB(config: LibConfig): void;
只要这个 .d.ts 文件被包含在你的项目中,你就可以在任何地方安全地调用 MY_GLOBAL_LIB(...),并获得类型检查。
总结
声明文件是 TypeScript 生态系统中不可或缺的粘合剂。它们是 TypeScript 能够理解和赋能庞大 JavaScript 世界的基石。
.d.ts文件只包含类型声明,没有实现。- 在动手编写前,务必先在
@types中搜索。 - 对于模块化的 JS 库,核心是使用
declare module '...'来包裹你的类型声明。 - 对于全局脚本,使用
declare var/let/const/function。
编写 .d.ts 文件可能起初看起来有些令人生畏,但它实际上是一个非常有价值的练习。它不仅能让你当前的项目变得更健壮,还能迫使你更深入地去理解你所依赖的 JavaScript 库的 API 设计。这是每一位专业 TypeScript 开发者都应具备的“超能力”。