24.声明文件.d.ts 副本.md

查看飞书原文 ↗3,926 字符

TypeScript 的强大之处在于其静态类型系统,它能在我们编码时就提供无与伦比的智能提示、错误检查和代码导航。但这引出了一个关键问题:当我们的 TypeScript 项目需要使用一个纯 JavaScript 编写的库(如一个 jQuery 插件、一个旧版的 npm 包,或者一个内部的工具库)时,会发生什么?

对于 TypeScript 编译器来说,这个 JavaScript 库就像一个“黑箱”。它不知道这个库导出了哪些函数,这些函数需要什么参数,又会返回什么。因此,编译器只能无奈地将所有从该库导入的东西都视为 any 类型。这意味着,我们失去了所有类型安全保障,回到了“刀耕火种”的 JavaScript 时代。

为了解决这个问题,TypeScript 提供了一套解决方案,其核心就是声明文件 (Declaration Files),它们以 .d.ts 为后缀。

什么是声明文件?

你可以将 .d.ts 文件想象成一本API 使用手册。它只描述 JavaScript 代码的“形状”(shape),而不包含任何具体的实现。它告诉 TypeScript 编译器:

它是一座桥梁,连接了 TypeScript 的静态类型世界和 JavaScript 的动态运行时世界。

在你动手写之前:先寻找现有的声明文件

在你准备为某个库从零开始编写 .d.ts 文件之前,请务必先做两件事,这能为你节省大量时间:

  1. 检查库本身:许多现代 JavaScript 库在发布时已经内置了自己的 .d.ts 文件。这是一个越来越普遍的最佳实践。
  2. 搜索 DefinitelyTyped:DefinitelyTyped 是一个庞大的、由社区驱动的开源项目,它为数千个流行的 JavaScript 库提供了高质量的 .d.ts 文件。这些文件都发布在 npm 的 @types scope 下。

例如,如果你想在项目中使用 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;
}

关键点:

第 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 文件可能起初看起来有些令人生畏,但它实际上是一个非常有价值的练习。它不仅能让你当前的项目变得更健壮,还能迫使你更深入地去理解你所依赖的 JavaScript 库的 API 设计。这是每一位专业 TypeScript 开发者都应具备的“超能力”。