likes
comments
collection
share

拿捏 TS 声明文件(上)

作者站长头像
站长
· 阅读数 16

从一个现象入手

AxiosLodash 是前端开发中常用的工具。在一个 TS 项目中导入并使用 axios 时,既不会报错,还会有语法提示,感觉很不错。

但是导入 lodash 时,就会得到一个红线的波浪线提示:

无法找到模块”lodash“的声明文件。

拿捏 TS 声明文件(上)

当然,编辑器也给出了解决方案:

尝试使用 npm i --save-dev @types/lodash (如果存在),或者添加一个包含 declare module 'lodash'; 的新声明(.d.ts)文件。

为什么 axios 可以正常用,而 lodash 就做了区别对待呢?这就要引入下面要介绍的关于类型声明和声明文件的内容了。我们先从给出的第二种解决方案入手。

TypeScript 模块

在解决这个问题之前,需要先了解 TypeScript 中模块的概念。

做前端开发的同学,对 CommonJS 和 ES Module 这两种模块化规范都不陌生。了解一个模块化规范,无非就是解决三个问题:

  1. 如何定义/导出一个模块
  2. 如何使用/导入一个模块
  3. 模块如何查找

我们重点看第三点,也就是模块是如何查找的。当所引入的模块,是一个非相对路径或者绝对路径,即不是一个 ./ 或者 / 开头的路径,此时对模块的解析策略,其实有两种。具体是哪种什么先不关心,大部分情况下,都是会从 node_modules 路径下去寻找同名目录。比如:

import axios from 'axios'

node_modules/axios 目录下,会检查它的 package.json 文件,这个文件中会有一个 types 字段,来指定此模块的类型声明文件:

"types": "index.d.ts",

这样编译器就知道了,此模块的声明文件是 index.d.ts ,就能识别出 axios模块,以及知道它导出了哪些方法供我们使用。

拿捏 TS 声明文件(上)

但是经过查看,并没有在 lodashpackage.json 中找到这个字段, TSC 找不到相关的类型,不能识别此模块,就只能报错了。

接着回到上面的第二种解决方案上。在项目根目录下有一个 env.d.ts 文件,它是脚手架提供的一个声明文件,添加如下内容:

declare module 'lodash';

declare 是一个关键字,用来声明一个“值”的类型,包括类,函数,变量,模块等。lodash 被认为是一个模块,所以使用 module 来标识。这样,我们就自己声明了一个模块类型,接着去查看下代码,发现已经不报错了:

拿捏 TS 声明文件(上)

但是在使用时会发现,缺少语法提示,还是比不上 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的形式命名,比如:

拿捏 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

此时再回到代码中,发现不仅不报错,还能有语法提示:

拿捏 TS 声明文件(上)

这都是因为安装了上面那个依赖。

@types 是一个包作用域,该名下的包都是一些常用工具库的声明文件,比如 lodashjquery。如果你使用的一个 JS 库缺乏相关的类型,就可以通过这种方案去安装第三方的声明文件。

@types/ 开头的包,默认会安装到 node_modules/@types 目录下,TS 编译器默认会读取此目录,从而得到类型。

总结一下

本文从一个简单的现象入手,延伸出了很多内容,主要是围绕 TypeScript 中的声明文件展开:

  1. 声明文件,主要解决一些没有类型的 JS 库的使用问题。
  2. 内置的声明文件可以直接使用,主要提供了环境相关的声明。
  3. 针对第三方库的声明文件,可以通过安装 @types/xxx 包来解决,它们会安装到 node_modules/@types 目录下,编译器会自动寻找和使用。
  4. 针对不存在第三方声明文件的库,可以自己手写 .d.ts 文件,并让编译器识别到声明文件,会涉及到一些编译选项的配置。

至于如何编写一份声明文件,这篇有点长了,换一篇讲,或者查看文档

感谢阅读 🍔