类型声明
我们提到了这么一条:“TypeScript 能够非常好地兼容 JavaScript 生态”,当时我们并没有去解释这句话,而是将其视为理所当然的。
但深入思考一下,你可能会产生一些疑问:JavaScript 生态指的是什么?TypeScript 的兼容又体现在哪里?
关于 JavaScript 的生态,编程语言的生态指的可不是生态圈,而是整个语言社区中,无数开发者提供的那些覆盖各种场景与用途的“包”,在 JavaScript 社区这指的当然就是 npm 包!作为世界上最大的编程语言包社区,npm 社区现在拥有超过 300w 个npm包。而 TypeScript 之所以如今能取得如此的成功,被广大 JavaScript 开发者接纳,一个不可忽视的原因就是它对 npm 包的兼容能力。
简单地说,一个 npm 包可能是在数年前编写发布的,作者已经不再维护,但它仍然拥有数千万的周下载量。
此时,如果作者希望提供 TypeScript 支持,让你在使用这个包的时候能获得类型提示,他并不需要分布一个新的版本,然后你再升级这个版本,只需要一点由 TypeScript 提供的黑科技——类型声明
就行。
此前我们学习的类型知识,都是和 TS 代码绑定,需要我们提前提供好的。
也即是说,必须标注这个变量为数组类型后,才能享受到后续对数组方法的提示,以及对数组类型的保障。
那么 TypeScript 又是怎么让我们不需要为 npm 包编写类型就能享受到类型提示的?类型声明此时闪亮登场,你可以理解为它是存在于全局的 TS 类型,当你访问数组变量,它会读取 Array 这个顶级对象在全局声明的类型,然后为你提供对应的类型提示,对于 npm 包也是如此。
类型声明
说了这么多概念,是时候来实战一下了。我们从一个日常使用频率非常高的库 Lodash 开始说起,首先安装它:
npm i lodash
然后我们在ts文件中导入它,此时你会发现 VSCode 会给出这么个提示:
无法找到模块“lodash”的声明文件。"/Users/linbudu/Desktop/0penSource/vite-project/node modules/.pnpm/lodash@4.17.21/node modules/lodash/lodasl隐式拥有"any"类型。
尝试使用npmi--save-dev @types/lodash`(如果存在),或者添加一个包含declare module 'lodash';的新声明(.d.ts)文件 ts(7016)
对应的是,我们使用 Lodash 这个导入时,没有任何提示。而先不管其他的,我们再安装一个模块 axios 看看:
npm i axios
你会发现,此时 axios 导入是包括了完整的类型声明的
为什么同样是 npm 包,一个无法提供类型提示,另一个却拥有非常完善的类型提示呢?回到 Lodash 导入给我们的报错,尝试按照提示安装下 @types/lodash 这个 npm 包
npm i @types/lodash
此时我们的类型提示就出现了!
现在我们可以来解释下这两个包存在的差异了。Lodash 就是我们上面说的那个下载量非常高,却缺少了 TypeScript 支持的 npm 包。
而 @types/lodash
就是 TypeScript 提供的黑魔法之一,我们先不需要理解这个 @types
开头的 npm 包到底是做什么的,只需要知道这个包内包含了提供给 Lodash 的「类型声明」
,当你安装这个包时,TypeScript 会自动地识别其中的类型声明,并在你导入 Lodash 这个包时使用这些类型声明作为提示。
也就是说,类型声明提供了一种独立于 JavaScript 代码之外,为 JavaScript 代码提供类型信息的方式。
那么上面的 axios 为什么不需要 @types/axios ?因为它选择了另一种方式,即通过将类型声明包含在 axios 这个包内的方式,这样,TypeScript 也能识别到其中的类型声明,并为 axios 这个导入提供类型提示。
而类型声明又是什么?打开 node_modules
中的 @types/lodash
,你会发现其中包含了一个个以 .d.ts
为后缀的文件:
这些文件是什么,看起来像 TS 文件,但又不完全像?实际上,类型声明这个概念在 TypeScript 中,需要专门的 .d.ts
文件来进行书写,这里的 d
即是 declaration
声明之意。
我们打开 @types/lodash/common/string.d.ts
,其中包含了 Lodash 中所有字符串相关方法的类型声明,简化后看起来是这样的:
declare module "lodash" {
camelCase(string?: string): string;
capitalize(string?: string): string;
endsWith(string?: string): string;
// ...
}
首先是 declare module "lodash"
,这是我们从未了解过的语法,可以称它为模块类型声明,它的作用其实就是告诉 TypeScript ,我们要为模块(module)lodash 进行类型声明(declare),在导入这个模块并访问属性时,你需要提示这个对象上具有 camelCase,capitalize 等方法
。
而模块类型声明不仅仅可以声明三方模块,还可以为一些非 JS/TS 类型的文件提供类型声明,比如我们后面会了解到的在 Vite 初始化项目中,为 CSS Modules
提供了类型声明:
// CSS modules
type CSSModuleClasses = { readonly [key: string]: string }
declare module '*.module.css' {
const classes: CSSModuleClasses
export default classes
}
declare module '*.module.scss' {
const classes: CSSModuleClasses
export default classes
}
declare module '*.module.sass' {
const classes: CSSModuleClasses
export default classes
}
// ...
通过上面的例子,我们已经能意识到类型声明的作用了。
概括地说,类型声明文件就是一种不包括任何实际逻辑,仅仅包含类型信息,并且无需导入操作,就能够被 TypeScript 自动加载的文件。
也就是说,如果定义了类型声明文件,即使你都不知道这个文件放在哪里了,其中的类型信息也能够被加载,然后成为你开发时的类型提示来源。
对变量的声明
除了模块声明以外,还有一种常见的声明是对变量的声明。比如你在 TS 文件中写个 window,然后尝试访问这个 window 的类型:
declare var window: Window & typeof globalThis;
interface Window {
// ...
}
你会发现,跳转到了 lib.dom.d.ts
文件,其中使用 declare var
这么个语法对 window 变量进行了类型声明。
首先,declare var
这个语法称为变量类型声明,我们知道 var
声明变量意味着这个变量在全局作用域可用,因此 declare var
自然也是将这个类型声明提供到全局,所以你才能在任何地方都访问到 window
这个变量的类型。
而 lib.dom.d.ts
文件,我们明明没有安装任何相关 npm 包,它又是哪来的?我们将 lib.xxx.d.ts
文件称为内置类型声明文件,它们是由 TypeScript 官方维护并提供的,用于描述 JavaScript 这门语言内置的顶级对象、方法以及 DOM API 等等
的类型声明文件。而这,就是我们此前在编写 TypeScript 时,能够一上来就获得事无巨细类型提示的原因。
而如果你已经尝试过使用 TypeScript 自带的编译器 tsc
,会发现当你编译一个 TS 文件时,它不仅仅会产生 JS 文件,还会产生一个 .d.ts
文件——也就是我们上面说到的类型声明文件。举例来说,这么一个 TS 文件:
export const name: string = 'xxx';
export function handler(input: string): void { };
export interface IUser {
name: string;
age: number;
}
export const users: IUser[] = [];
会被编译为一个 JS 文件和一个声明文件:
// .js
export const name = 'linbudu';
export function handler(input) { }
;
export const users = [];
// .d.ts
export declare const name: string;
export declare function handler(input: string): void;
export interface IUser {
name: string;
age: number;
}
export declare const users: IUser[];
也就是说,在从 TS 编译到 JS 的过程中,类型并不是真的全部消失了,而是被放到了专门的类型声明文件里。为什么要这么设计?当然是为了和上面 npm 包类型定义一样的效果,也就是在别人是使用 JS 代码调用你的编译产物时,既可以保证直接能够运行,又可以通过类型声明提供完整的类型信息。
两种场景只是由一段代码是作为消费者还是生产者来决定的,如果你是消费者,需要声明文件作为类型信息的提供方,如果你是生产者,为了用户的五星好评开发体验,则要记得生成类型声明文件。