第 11 章 声明文件

Flying
2022-11-26 / 0 评论 / 121 阅读 / 正在检测是否收录...

声明文件
具有纯类型系统代码
无运行时构造

尽管用 TypeScript 编写代码很棒,这就是你想要做的,但你需要能够在 TypeScript 项目中处理原始 JavaScript 文件。许多包是直接用 JavaScript 编写的,而不是 TypeScript。即使是用 TypeScript 编写的包也会作为 JavaScript 文件分发。

此外,TypeScript 项目需要一种方法来告诉环境特定功能的类型形状,例如全局变量和API。例如,在 Node.js 中运行的项目可以访问浏览器中不可用的内置 Node 模块,反之亦然。

TypeScript 允许将类型形状与其实现分开声明。类型声明通常写在名称以 .d.ts 扩展名结尾的文件(称为声明文件)中。声明文件通常要么在项目中编写,要么与项目的编译 npm 包一起构建和分发,要么作为独立的“typings”包共享。

声明文件

.d.ts 声明文件的工作方式通常与 .ts 文件类似,但有一个明显的限制,即不允许包含运行时代码。.d.ts 文件仅包含可用运行时值、接口、模块和常规类型的描述。它们不能包含任何可以编译为 JavaScript 的运行时代码。

声明文件可以其他任何 TypeScript 源文件一样导入。

这个 types.d.ts 文件导出了一个由 index.ts 文件使用的 Character 接口:

// types.d.ts
export interface Character {
  catchphrase?: string; 
  name: string;
}
// index.ts
import { Character } from "./types";


export const character: Character = {
  catchphrase: "Yee-haw!",
  name: "Sandy Cheeks”,
};
声明文件创建所谓的环境上下文,即只能声明类型而不能声明值的代码区域。

本章主要介绍声明文件以及其中最常用的类型声明形式。

声明运行时值

尽管定义文件可能不会创建运行时值(如函数或变量),但它们可以使用 declare 关键字声明这些构造存在。这样做会告诉类型系统,某些外部影响(如网页中的<script>标记)已在具有特定类型的该名称下创建了值。

使用 declare 声明变量使用与普通变量声明相同的语法,但不允许使用初始值。

此代码段成功声明了 declared 变量,但在尝试为 initializer 变量提供值时收到类型错误:

// types.d.ts
declare let declared: string; // Ok

declare let initializer: string = "Wanda";
//    ~~~~~~~
// 错误:不允许在环境上下文中使用初始化表达式。

函数和类的声明方式与其常规形式类似,但没有函数或方法的主体。

以下 canGrantWish 函数和方法是正确声明的,不包含函数体,但 grantWish 函数和方法报语法错误,为试图不当地设置一个函数体:

// fairies.d.ts
declare function canGrantWish(wish: string): boolean; // Ok

declare function grantWish(wish: string) { return true; }
//    ~
// 错误:不能在环境上下文中声明实现。

class Fairy {
  canGrantWish(wish: string): boolean; // Ok

  grantWish(wish: string) {
    //    ~
    // 错误:函数实现名称必须为“canGrantWish”。
    return true;
  }
}
TypeScript 的隐式 any 规则对于在环境上下文中声明的函数和变量的工作方式与在普通源代码中相同。由于环境上下文可能不提供函数体或初始变量值,因此显式类型注解(包括显式返回类型注解)通常是阻止它们隐式成为 any 类型的唯一方法。

尽管使用 declare 关键字的类型声明在 .d.ts 定义文件中最常见,但 declare 关键字也可以在声明文件外部使用。模块或脚本文件也可以使用 declare。当全局可用变量仅用于该文件时,这可能很有用。

在这里,在 index.ts 文件中定义了 myGlobalValue 变量,因此允许在该文件中使用:

// index.ts
declare const myGlobalValue: string;

console.log(myGlobalValue); // Ok

请注意,虽然在 .d.ts 定义文件中允许带或不带 declare 的类型形状(如接口),但运行时不带 declare 构造(如函数或变量)将触发类型错误:

// index.d.ts
interface Writer {} // Ok
declare interface Writer {} // Ok

declare const fullName: string; // 正确:类型是原始字符串
declare const firstName: "Liz"; // 正确:类型是是字面量“value”

const lastName = "Lemon";
// 错误:.d.ts 文件中的顶级声明必须以 "declare" 或 "export" 修饰符开头。

全局值

由于没有 importexport 语句的 TypeScript 文件被视为脚本而不是模块,因此在其中声明的构造(包括类型)全局可用。没有任何导入或导出的定义文件可以利用该行为全局声明类型。全局定义文件对于声明应用程序中所有文件中可用的全局类型或变量特别有用。

在这里,globals.d.ts 文件声明 const version: string 全局存在。然后,version.ts 文件能够引用全局 version 变量,尽管没有从 globals.d.ts 导入:

// globals.d.ts
declare const version: string;
// version.ts
export function logVersion() { 
  console.log(`Version: ${version}`); // Ok
}

全局声明的值最常用于使用全局变量的浏览器应用程序。尽管大多数现代 Web 框架通常使用较新的技术,例如 ECMAScript 模块,但它仍然很有用,尤其是在较小的项目中,能够全局存储变量。

如果发现无法自动访问 .d.ts 文件中声明的全局类型,请仔细检查 .d.ts 文件是否未导入和导出任何内容。即使是一次导出也会导致整个文件不再全局可用!

全局接口合并

变量不是 TypeScript 项目类型系统中唯一的全局元素。对于全局 API 和值,存在许多类型声明。因为接口与同名的其他接口合并,因此在全局脚本上下文中声明接口(例如没有任何 importexport 语句的 .d.ts 声明文件)会全局增加该接口。

例如,依赖于服务器设置的全局变量的 Web 应用程序可能希望将其声明为全局 Window 接口上存在。接口合并将允许像 types/window.d.ts 这样的文件声明一个类型为 Window 的全局变量,该变量存在于全局 window 变量中:

<script type="text/javascript" > 
  window.myVersion = "3.1.1";
</script>
// types/window.d.ts
interface Window {
  myVersion: string;
}

// index.ts
export function logWindowVersion() {
  console.log(`Window version is: ${window.myVersion}`);
  window.alert("Built-in window types still work! Hooray!")
}

全局增强

.d.ts 文件中同时需要 importexport 语句和扩展全局作用域并不总是可行的,例如,当通过导入在其他地方定义的类型来大大简化全局定义时。有时在模块文件中声明的类型是要在全局使用。

对于这些情况,TypeScript 允许使用 declare global 语法来声明一个代码块是全局的。这样做将该块的内容标记为全局上下文,即使其周围环境不是全局的:

// types.d.ts
// (module context)

declare global {
  // (global context)
}

// (module context)

在这里,types/data.d.ts 文件导出 Data 接口,稍后将由 types/globals.d.ts 和运行时 index.ts 导入:

// types/data.d.ts
export interface Data {
  version: string;
}

此外,types/globals.d.tsdeclare global 块内全局声明 Data 类型的变量,以及仅在该文件中可用的变量:

// types/globals.d.ts
import { Data } from "./data";

declare global {
  const globallyDeclared: Data;
}

declare const locallyDeclared: Data;

然后,index.ts 无需导入即可访问 globallyDeclared 变量,但仍然需要导入 Data

// index.ts
import { Data } from "./types/data";

function logData(data: Data) { // Ok
console.log(`Data version is: ${data.version}`);
}

logData(globallyDeclared); // Ok

logData(locallyDeclared);
//    ~~~~~~~~~~~~~~~
// 错误:找不到名称“locallyDeclared”。

让全局和模块声明以很好地协同工作可能很棘手。正确使用 TypeScript 的 declareglobal 关键字可以描述哪些类型定义在项目中是全局可用的。

内置声明

现在你已经了解了声明的工作原理,是时候揭开它们在 TypeScript 中的隐藏用途了:它们一直在为其类型检查提供支持!全局对象(如 ArrayFunctionMapSet 是类型系统需要了解但未在代码中声明的构造示例。它们由你的代码的任何运行时提供:比如 Deno、Node、Web 浏览器等。

库声明

存在于所有 JavaScript 运行时中的内置全局对象(如 ArrayFunction)在名称为 lib.[target].d.ts 的文件中声明。target 是针对你的项目 JavaScript 的最低支持版本,例如 ES5、ES2020 或 ESNext。

内置库定义文件或“lib 文件”相当大,因为它们代表了整个 JavaScript 的内置 API。例如,内置 Array 类型的成员由全局 Array 接口表示,该接口如下所示:

// lib.es5.d.ts

interface Array<T> {
  /**
  * Gets or sets the length of the array.
  * This is a number one higher than the highest index in the array.
  */
  length: number;

  // ...
}

Lib 文件作为 TypeScript npm 包的一部分分发。你可以在软件包中的 modules/typescript/lib/lib.es5.d.ts 等路径中找到它们。对于像 VS Code 这样使用自己打包的 TypeScript 版本来键入检查代码的 IDE,可以通过右键单击代码中的内置方法(如数组的 forEach)并选择“转到定义”等选项来找到正在使用的 lib 文件(图 11-1)。

figure-11-1.jpg
图 11-1。左:转到 forEach 的定义;右:结果打开了 lib.es5.d.ts 文件

库目标

默认情况下,TypeScript 将包含基于 tsc CLI 和/或项目的 tsconfig.json 中的 target 设置(默认情况下为 “es5”)的相应 lib 文件。较新版本的 JavaScript 的连续 lib 文件使用接口合并相互构建。

例如,在 lib.es2015.d.ts 中 列出 ES2015 中添加的静态 Number 成员(如 EPSILONisFinite):

// lib.es2015.d.ts

interface NumberConstructor {
  /**
  * The value of Number.EPSILON is the difference between 1 and the
  * smallest value greater than 1 that is representable as a Number
  * value, which is approximately:
  * 2.2204460492503130808472633361816 x 10−16.
  */
  readonly EPSILON: number;

  /**
  * Returns true if passed value is finite.
  * Unlike the global isFinite, Number.isFinite doesn't forcibly
  * convert the parameter to a number. Only finite values of the
  * type number result in true.
  * @param number A numeric value.
  */
  isFinite(number: unknown): boolean;

  // ...
}

TypeScript 项目将包含 JavaScript 的所有版本目标的 lib 文件,直至其最小目标。例如,目标为 “es2016” 的项目将包括 lib.es5.d.ts、lib.es2015.d.ts 和 lib.es2016.d.ts

仅在比目标版本更新的 JavaScript 版本中可用的语言功能在类型系统中不可用。例如,如果你的目标是“es5”,则无法识别 ES2015 或更高版本的语言功能(如 String.prototype.startsWith)。

编译器选项(如 target)在第 13 章 “配置选项”中有更详细的介绍。

DOM 声明

在 JavaScript 语言本身之外,类型声明最常引用的区域是 Web 浏览器。Web 浏览器类型(通常称为“DOM”类型)涵盖 API(如 localStorage)和类型形状(如 HTMLElement),主要在 Web 浏览器中可用。DOM 类型与其他 lib.d.ts 声明文件一起存储在 lib.dom.d.ts 文件中。

全局 DOM 类型与许多内置全局变量一样,通常使用全局接口进行描述。例如,Storage 接口用于 localStoragesessionStor age,大致按如下方式开始:

// lib.dom.d.ts

interface Storage {
  /**
  * Returns the number of key/value pairs.
  */
  readonly length: number;

  /**
  * Removes all key/value pairs, if there are any.
  */
  clear(): void;

  /**
  * Returns the current value associated with the given key,
  * or null if the given key does not exist.
  */
  getItem(key: string): string | null;

  // ...
}

TypeScript 在不覆盖 lib 编译器选项的项目中默认包含 DOM 类型。对于要在非浏览器环境(如 Node)中运行的项目,有时这会让开发者感到困惑,因为他们应该无法访问全局 API,例如类型系统会声称存在的 documentlocalStorage。编译器选项(如 lib)在第 13 章 “配置选项”中有更详细的介绍。

模块声明

声明文件的另一个重要功能是它们能够描述模块的形状。declare 关键字可以在模块的字符串名称之前使用,以通知类型系统该模块的内容。

在这里,“my-example-lib” 模块声明存在于 modules.d.ts 声明脚本文件中,然后在 index.ts 文件中使用:

// modules.d.ts
declare module "my-example-lib" {
  export const value: string;
}
// index.ts
import { value } from "my-example-lib";


console.log(value); // Ok

你不必在自己的代码中经常使用 declare module(如果有的话)。它主要和下一节的通配符模块声明及本章后面介绍的包类型一起使用。
此外,有关 resolveJsonModule 的信息,请参见第 13 章 “配置选项”,它是一个编译器选项,允许 TypeScript 本机识别来自 .json 文件的导入。

通配符模块声明

模块声明的常见用途是告诉 Web 应用程序特定的非 JavaScript/TypeScript 文件扩展名可用于 import 进入代码。模块声明可能包含单个 * 通配符,表示任何匹配该模式的模块看起来都是相同的。

例如,许多 Web 项目(例如在流行的 React 启动器(如 create-react-app 和 create-next-app )中预配置的项目)支持 CSS 模块将 CSS 文件中的样式作为可在运行时使用的对象导入。他们将使用诸如 “*.module.css” 的模式定义模块,该模式默认导出类型为 { [i: string]: string }

// styles.d.ts
declare module "*.module.css" {
  const styles: { [i: string]: string };
  export default styles;
}

// component.ts
import styles from "./styles.module.css";
 
styles.anyClassName; // Type: string
使用通配符模块来表示本地文件并不完全类型安全。TypeScript 不提供确保导入的模块路径与本地文件匹配的机制。一些项目使用构建系统(如 Webpack)和/或从本地文件生成 .d.ts 文件,以确保导入匹配。

包类型

现在,你已经了解了如何在项目中声明类型,现在是时候介绍包之间的使用类型了。用 TypeScript 编写的项目通常仍然分发包含已编译的 .js 输出的包。它们通常使用 .d.ts 文件来声明这些 JavaScript 文件背后的 TypeScript 类型系统形状。

声明

TypeScript 提供了一个 declaration 的选项,用于创建与 JavaScript 输出并行的 .d.ts 输出的输入文件。

例如,给定以下 index.ts 源文件:

// index.ts
export const greet = (text: string) => {
  console.log(`Hello, ${text}!`);
};

使用模块“es2015”和目标“es2015”的声明,将生成以下输出:

// index.d.ts
export declare const greet: (text: string) => void;
// index.js
export const greet = (text) => {
  console.log(`Hello, ${text}!`);
};
自动生成的 .d.ts 文件是项目创建供使用者使用的类型定义的最佳方式。通常建议大多数用 TypeScript 编写的生成 .js 文件输出的包也应该将 .d.ts 与这些文件捆绑在一起。第 13 章 “配置选项”中详细介绍了编译器选项(如 declaration)。

依赖包类型

TypeScript 能够检测和利用捆绑在项目 node_modules 依赖项中的 .d.ts 文件。这些文件将通知类型系统该包导出的类型形状,就好像它们是在同一项目中编写的或使用 declare 模块声明的一样。

带有自己的 .d.ts 声明文件的典型 npm 模块可能具有如下文件结构:

lib/
  index.js 
  index.d.ts
package.json

例如,广受欢迎的测试运行器 Jest 是用 TypeScript 编写的,并在其 jest 包中提供了自己的捆绑 .d.ts 文件。它依赖于 @jest/globals 包,该包提供 describeit 等函数,然后 jest 使其全局可用:

// package.json
{
  "devDependencies": {
    "jest": "^32.1.0"
  }
}
// using-globals.d.ts
describe("MyAPI", () => {
  it("works", () => { / ... */ });
});

// using-imported.d.ts
import { describe, it } from "@jest/globals";

describe("MyAPI", () => {
  it("works", () => { /* ... */ });
});

如果我们从头开始重新创建 Jest 类型包的一个非常有限的子集,它们可能看起来像这些文件。@jest/globals 包导出 describeit 函数。然后,jest 包导入这些函数,并使用其相应函数类型的 describeit 变量扩展全局范围:

// node_modules/@jest/globals/index.d.ts
export function describe(name: string, test: () => void): void;
export function it(name: string, test: () => void): void;

// node_modules/jest/index.d.ts
import * as globals from "@jest/globals";

declare global {
  const describe: typeof globals.describe;
  const it: typeof globals.it;
}

此结构允许使用 Jest 的项目引用 describeit 的全局版本。项目也可以选择从 @jest/globals 包中导入这些函数。

公开包类型

如果你的项目打算在 npm 上分发并为使用者提供类型,请在包的 package.json 文件中添加 “types” 字段以指向根声明文件。types 字段的工作方式与 main 字段类似,通常看起来相同,但扩展名为 .d.ts 而不是 .js

例如,在此 fictional 包文件中,./lib/index.js 主运行时文件与 .lib/index.d.ts 类型文件并行运行:

{
  "author": "Pendant Publishing",
  "main": "./lib/index.js",
  "name": "coffeetable",
  "types": "./lib/index.d.ts",
  "version": "0.5.22",
}

TypeScript 将使用 ./lib/index.d.ts 的内容作为从 utilitarian 包导入消费文件时应该提供的内容。

如果包的 package.json 中不存在 types 字段,TypeScript 将采用默认值 ./index.d.ts。这与 npm 默认行为相似,如果未指定,则假定 ./index.js 文件是包的主入口点。

大多数软件包使用 TypeScript 的 declaration 编译器选项来创建 .d.ts 文件以及输出源文件 .js 。编译器选项在第 13 章 “配置选项”中介绍。

DefinitelyTyped

可悲的是,并非所有项目都是用 TypeScript 编写的。一些不幸的开发者仍然用普通的 JavaScript 编写他们的项目,而没有类型检查器来帮助他们。令人毛骨悚然

我们的 TypeScript 项目仍然需要了解这些包中的模块类型。TypeScript 团队和社区创建了一个巨大的仓库,叫做 DefinitelyTyped ,用于存储社区作者为包编写的定义。DefinitelyTyped 简称 DT,是 GitHub 上最活跃的存储库之一。它包含数千个 .d.ts 定义的包,以及围绕审查更改提案和发布更新的自动化流程。

DT 包以与提供类型的包相同的名称发布在 npm 上的 @types 作用域下。例如,截至 2022 年,@types/reactreact 包提供类型定义。

@types 通常作为 dependenciesdevDependencies 安装,尽管近年来这两者之间的区别变得模糊。一般来说,如果项目要作为 npm 包分发,则应使用依赖项,以便包的使用者也引入其中使用的类型定义。如果项目是独立的应用程序(例如在服务器上构建和运行的应用程序),则应使用 devDependencies 来传达类型只是一个开发时工具。

例如,对于依赖于 lodash 的实用程序包,截至 2022 年,该实用程序包具有单独的 @types/lodash 包,package.json 将包含类似于以下内容的行:

// package.json
{
  "dependencies": {
    "@types/lodash": "^4.14.182", 
    "lodash": "^4.17.21",
  }
}

基于 React 构建的独立应用程序的 package.json 可能包含类似于以下内容的行:

// package.json
{
  "dependencies": {
    "react": "^18.1.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.9"
  },
}

请注意,@types/ 包和它们所表示的包之间的语义版本号(“semver”)不一定匹配。你可能经常发现某些包的语义化版本号与其所代表的包的版本号不同,有些可能相差一个补丁版本,如前面所述的 React,有些可能相差一个次要版本,如前面所述的 Lodash,甚至可能相差一个主要版本。

由于这些文件是由社区编写的,因此它们可能落后于父项目或存在小的不准确之处。如果你的项目编译成功但在调用库时出现运行时错误,请检查你所访问的 API 的签名是否发生了变化。这在稳定的 API 上,成熟的项目中较少见,但仍然不是不可能的。

类型可用性

大多数流行的 JavaScript 包要么带有自己的类型,要么通过 DefinitelyType 提供类型。

如果要获取尚无可用类型的包的类型,则三个最常见的选项是:

  • 向 DefinitelyTyped 发送拉取请求以创建其 @types/ 包。
  • 在项目中使用前面介绍的 declare module 语法编写类型。
  • 禁用本书第 13 章 “配置选项”中介绍的 noImplicitAny 配置,不过该章节也强烈建议不要这么做。

如果你有时间,我建议将类型贡献给 DefinitelyTyped。这样做可以帮助其他可能也想使用该包的 TypeScript 开发者。

访问 aka.ms/types 以查看包是捆绑类型还是通过单独的@types/ 包。

总结

在本章中,你使用声明文件和值声明来通知 TypeScript 有关没有在源代码中声明的模块和值:

  • 使用 .d.ts 创建声明文件
  • 使用 declare 关键字声明类型和值
  • 使用全局值、全局接口合并和全局增强来更改全局类型
  • 配置并使用 TypeScript 内置的目标、库和 DOM 声明
  • 声明模块的类型,包括通配符模块
  • TypeScript 如何从包中选取类型
  • 使用 DefinitelyTyped 获取不包含其自身包的类型
现在你已经读完了这一章,你最好练习一下学到的东西 https://learningtypescript.com/declaration-files

TypeScript 类型在美国南部说什么?
“我要声明!”

1

评论 (0)

取消