拿捏 TS 声明文件(上)
从一个现象入手
Axios
和 Lodash
是前端开发中常用的工具。在一个 TS 项目中导入并使用 axios
时,既不会报错,还会有语法提示,感觉很不错。
但是导入 lodash
时,就会得到一个红线的波浪线提示:
无法找到模块”lodash“的声明文件。
当然,编辑器也给出了解决方案:
尝试使用
npm i --save-dev @types/lodash
(如果存在),或者添加一个包含declare module 'lodash';
的新声明(.d.ts)文件。
为什么 axios 可以正常用,而 lodash 就做了区别对待呢?这就要引入下面要介绍的关于类型声明和声明文件的内容了。我们先从给出的第二种解决方案入手。
TypeScript 模块
在解决这个问题之前,需要先了解 TypeScript 中模块的概念。
做前端开发的同学,对 CommonJS 和 ES Module 这两种模块化规范都不陌生。了解一个模块化规范,无非就是解决三个问题:
- 如何定义/导出一个模块
- 如何使用/导入一个模块
- 模块如何查找
我们重点看第三点,也就是模块是如何查找的。当所引入的模块,是一个非相对路径或者绝对路径,即不是一个 ./
或者 /
开头的路径,此时对模块的解析策略,其实有两种。具体是哪种什么先不关心,大部分情况下,都是会从 node_modules
路径下去寻找同名目录。比如:
import axios from 'axios'
在 node_modules/axios
目录下,会检查它的 package.json
文件,这个文件中会有一个 types
字段,来指定此模块的类型声明文件:
"types": "index.d.ts",
这样编译器就知道了,此模块的声明文件是 index.d.ts
,就能识别出 axios
模块,以及知道它导出了哪些方法供我们使用。
但是经过查看,并没有在 lodash
的package.json
中找到这个字段, TSC 找不到相关的类型,不能识别此模块,就只能报错了。
接着回到上面的第二种解决方案上。在项目根目录下有一个 env.d.ts
文件,它是脚手架提供的一个声明文件,添加如下内容:
declare module 'lodash';
declare
是一个关键字,用来声明一个“值”的类型,包括类,函数,变量,模块等。lodash
被认为是一个模块,所以使用 module
来标识。这样,我们就自己声明了一个模块类型,接着去查看下代码,发现已经不报错了:
但是在使用时会发现,缺少语法提示,还是比不上 axios
。
同时也会有新的疑问产生,那个 env.d.ts
是什么?什么是声明文件?TSC 是如何知道它的存在的?
声明文件
在 TypeScript 项目中,经常可以看到 .d.ts
为后缀的文件。这里的 d
表示 declaration
,声明的意思,这些文件就是 TypeScript 的类型声明文件。那么它的作用是什么呢?
通常来说,我们自己编写 .ts
文件时,都会写好相关的类型,再去使用。这自然不会有什么问题。
对于一些诞生于较早时期的工具库,比如上面提到的 lodash
,它们是使用原生 JS 实现的,缺少类型,那么在 TS 项目中导入时,就会缺少相应的类型信息,所以编辑器才会报错。而 .d.ts
这些声明文件,就是为了这个问题而诞生的。也就是,为一些缺少类型的 .js
文件提供类型声明,从而在 TS 项目中使用。
在声明文件中,我们可以通过 declare
关键字来为比变量,函数,类,模块,命名空间来声明类型,比如:
declare module 'lodash';
这样就声明了一个模块,TS 编译器可以读取到 env.d.ts
这个文件,从而知道存在一个叫做 lodash
的模块,所以编辑器就不会报错了。
声明文件的类型
外部声明文件
默认情况下,在一个干净的 TS 项目中,比如:
mkdir ts-project
cd ts-project
pnpm init
tsc --init
TS 编译器会自动读取目录下所有的 .ts
和 .d.ts
文件。
但是在 create-vue 脚手架创建的 Vue +TS 项目中,针对解析策略做了一定的配置,查看 tsconfig.json
:
{
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"]
}
使用 include
选项限制了编译器读取哪些文件,可以看到,默认会读取根目录下的 env.d.ts
,整个 src
目录和项目中所有的 .vue
文件 。
那么此时,如果要声明一些类型,要么直接添加到 env.d.ts
文件中,要么自定义 *.d.ts
文件后,再把它添加到 include
配置项中。
像这种开发者自定义的 .d.ts
声明文件,叫做外部声明文件。可以指定一些策略,让 TSC 能正确的解析。
内部声明文件
除了自己手写 .d.ts
文件,TS 也把一些环境的类型文件内置了,比如浏览器环境的 document
对象,比如 JS 中常用的各种对象方法,我们都是可以直接用的,这是因为在安装 typescript 时,会把这些声明文件一并安装,并以 lib.xxx.d.ts
的形式命名,比如:
lib.dom.d.ts
里面是和 DOM 相关的类型声明,lib.es2015.d.ts
是整个 ES2015 的声明文件,而 lib.es2015.promise.d.ts
就只包含了和 Promise 对象相关的类型声明。这些都是内置声明文件。
第三方声明文件
再把目光放回最开始的问题,除了自己声明一个类型外,还可以通过安装一个包来解决不能识别 lodash
模块的问题。
去掉 env.d.ts
中的自定义声明,安装下面的依赖:
$ pnpm add -D @types/lodash
此时再回到代码中,发现不仅不报错,还能有语法提示:
这都是因为安装了上面那个依赖。
@types
是一个包作用域,该名下的包都是一些常用工具库的声明文件,比如 lodash
,jquery
。如果你使用的一个 JS 库缺乏相关的类型,就可以通过这种方案去安装第三方的声明文件。
以@types/
开头的包,默认会安装到 node_modules/@types
目录下,TS 编译器默认会读取此目录,从而得到类型。
总结一下
本文从一个简单的现象入手,延伸出了很多内容,主要是围绕 TypeScript 中的声明文件展开:
- 声明文件,主要解决一些没有类型的 JS 库的使用问题。
- 内置的声明文件可以直接使用,主要提供了环境相关的声明。
- 针对第三方库的声明文件,可以通过安装
@types/xxx
包来解决,它们会安装到node_modules/@types
目录下,编译器会自动寻找和使用。 - 针对不存在第三方声明文件的库,可以自己手写
.d.ts
文件,并让编译器识别到声明文件,会涉及到一些编译选项的配置。
至于如何编写一份声明文件,这篇有点长了,换一篇讲,或者查看文档。
感谢阅读 🍔
转载自:https://juejin.cn/post/7222189908429062181