声明文件
具有纯类型系统代码
无运行时构造
尽管用 TypeScript 编写代码很棒,这就是您想要做的,但您需要能够在 TypeScript 项目中处理原始 JavaScript 文件。许多包是直接用 JavaScript 编写的,而不是 TypeScript。即使是用 TypeScript 编写的包也会作为 JavaScript 文件分发。
此外,TypeScript 项目需要一种方法来告诉环境特定功能(如全局变量和 API)的类型形状。例如,在 Node.js 中运行的项目可以访问浏览器中不可用的内置 Node 模块,反之亦然。
TypeScript 允许将类型形状与其实现分开声明。类型声明通常写在名称以 .d.ts 扩展名结尾的文件(称为声明文件)中。声明文件通常要么在项目中编写,要么与项目的编译 npm 包一起构建和分发,要么作为独立的“类型”包共享。
声明文件
.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";
// ~~~~~~~
// Error: Initializers are not allowed in ambient contexts.
函数和类的声明也类似于它们的正常形式,但没有函数或方法的主体。
以下 canGrantWish
函数和方法在没有主体的情况下正确声明,但 grantWish
函数和方法因尝试设置主体二报语法错误:
// fairies.d.ts
declare function canGrantWish(wish: string): boolean; // Ok
declare function grantWish(wish: string) { return true; }
// ~
// Error: An implementation cannot be declared in ambient contexts.
class Fairy {
canGrantWish(wish: string): boolean; // Ok
grantWish(wish: string) {
// ~
// Error: An implementation cannot be declared in ambient contexts.
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; // Ok: type is the primitive string
declare const firstName: "Liz"; // Ok: type is the literal "value"
const lastName = "Lemon";
// Error: Top-level declarations in *.d.ts* files must
// start with either a 'declare' or 'export' modifier.
全局值
由于没有 import
或 export
语句的 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 和值存在许多类型声明。由于接口与同名的其他接口合并,因此在全局脚本上下文(例如没有任何 import
或 export
语句的 .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 文件中避免使用 import
或 export
语句并不总是可行的,例如,当通过导入在其他地方定义的类型来大大简化全局定义时。有时,在模块文件中声明的类型旨在全局使用。
对于这些情况,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.ts
在 declare 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);
// ~~~~~~~~~~~~~~~
// Error: Cannot find name 'locallyDeclared'.
让全局和模块声明以很好地协同工作可能很棘手。正确使用 TypeScript 的 declare
和 global
关键字可以描述哪些类型定义在项目中是全局可用的。
内置声明
现在您已经了解了声明的工作原理,是时候揭开它们在 TypeScript 中的隐藏用途了:它们一直在为其类型检查提供支持!全局对象(如 Array
、Function
、Map
和 Set
是类型系统需要了解但未在代码中声明的构造示例。它们由您的代码的任何运行时提供:比如 Deno、Node、Web 浏览器等。
库声明
存在于所有 JavaScript 运行时中的内置全局对象(如 Array
和 Function
)在名称为 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)。
图 11-1。左:转到 forEach
的定义;右:结果打开了 lib.es5.d.ts 文件
库目标
默认情况下,TypeScript 将包含基于 tsc
CLI 和或项目的 tsconfig.json 中的 target
设置(默认情况下为 “es5”
)的相应 lib 文件。较新版本的 JavaScript 的连续 lib 文件使用接口合并相互构建。
例如,在 lib.es2015.d.ts 中 列出 ES2015 中添加的静态 Number
成员(如 EPSILON
和 isFinite
):
// 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
接口用于 localStorage
和 sessionStor 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,例如类型系统会声称存在的 document
和 localStorage
。编译器选项(如 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
的选项,用于为输入文件创建 .d.ts 输出以及 JavaScript 输出。
例如,给定以下 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
包,该包提供 describe
和 it
等函数,然后 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
包导出 describe
和 it
函数。然后,jest
包导入这些函数,并使用其相应函数类型的 describe
和 it
变量扩充全局范围:
// 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 的项目引用 describe
和 it
的全局版本。项目也可以选择从 @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/react
为 react
包提供类型定义。
@types 通常作为依赖项或开发依赖安装,尽管近年来这两者之间的区别变得模糊。一般来说,如果项目要作为 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"
},
}
请注意,语义版本控制 (“semver”) 编号在 @types/
包和它们所表示的包之间不一定匹配。您可能经常会发现一些补丁版本(如早期的 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 类型在美国南部说什么?
“我要声明!”
评论 (0)