编写声明文件

当一个第三方库没有提供声明文件时,这时我们就需要自己去编写声明文件。在不同的场景下,声明文件的内容和使用方式会有所不同。下满我们会逐一介绍到。

全局变量

全局变量的类型声明是最简单的一种场景,前面我们介绍到的 jQuery 的声明就是全局变量声明的一种方式。

// 声明全局变量 declare var、declare const、declare let
declare const username: "admin" | "user";

// 声明全局方法,支持重载方法
declare function print(value: string): void;
declare function print(value: number): void;

// 声明全局类
declare class User {
  name: string;
  age: number;

  constructor(name: string, age: number);

  printInfo(): void;
}

// 声明全局枚举类型
declare enum Direction {
  Up,
  Down,
  Left,
  Right
}

// 声明全局类型
declare type TaskStatus = "pending" | "running" | "completed" | "failed";

// 申明全局接口
declare interface Task {
  id: number;
  name: string;
  status: TaskStatus;
}

命名空间

namespace 是 TypeScript 早期为了解决模块化而创造的关键字。在还没有 ES6 的时候,TypeScript 提供了一种模块化方法,使用 module 关键字来定义模块。由于后来 ES6 的广泛应用,TypeScript 在 ES6 模块化语法的基础上,又提供了 namespace 关键字来定义模块,从而替换掉了 module 关键字。随着 ES6 的广泛应用, namespace 现已不再推荐使用,更推荐使用 ES6 的模块化语法。

虽然namespace 现不再推荐使用,但在声明文件中还是比较常用的,它通常用来表示全局变量是一个对象,包含很多字属性。

嵌套的命名空间

如果一个对象拥有多个属性,并且每个属性又是一个对象,我们可以通过嵌套 namespace 来表示。

interface 和 type

在声明文件中,除了全局变量之外,还会有一些其它类型也希望暴露出来,在类型声明文件中,我们可以通过使用 interfacetype 关键字来声明一个全局接口或类型。

声明合并

假如 jQuery 既是一个函数,又是一个拥有 ajax 方法的对象,那么我们可以组合多个声明语句,它们会不冲突的合并起来。

npm 包

在项目中我们通过 import axios from 'axios' 导入一个 npm 包,这是符合 ES6 模块规范的。在我们尝试给一个第三方模块创建声明文件之前,需要查看一下它的声明文件是否已经提供了。

一般来说,npm 包的声明文件可能存在于两个地方:

  1. 声明文件与该包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。

  2. 发布到 @types 里。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是 npm install @types/axios --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。

假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:

  1. 创建一个 node_modules/@types/axios/index.d.ts 文件,存放 axios 模块的声明文件。这种方式不需要额外的配置,但是 node_modules 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。

  2. 创建一个 types 目录,专门用来管理自己写的声明文件,将 axios 的声明文件放到 types/axios/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 pathsbaseUrl 字段。

配置好之后,通过 import 导入 axios 的时候,TypeScript 就会去 types 目录下寻找对应的模块的声明文件了。

注意: module 配置可以有很多种选项,不同的选项会影响模块的导入导出模式。这里我们使用了 commonjs 这个最常用的选项,后面的内容都将以这个作为默认选项。

npm 包的声明文件主要有以下几种语法:

  • export导出变量

  • export namespace导出(含有子属性的)对象

  • export defaultES6 默认导出

  • export = commonjs 导出模块

export

npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 declare 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 export 导出,然后在使用方 import 导入后,才会应用到这些类型声明。

export 的语法与普通的 typeScript 中的语法类似,区别仅在于声明文件中禁止定义具体的实现:

对应的导入和使用模块应该是这样:

混用 declare 和 export

我们也可以使用 declare 先声明多个变量,最后再用 export 一次性导出。上例的声明文件可以等价的改写为

注意,与全局变量的声明文件类似,interface 前是不需要 declare 的。

export namespace

declare namespace 类似,export namespace 用来导出一个拥有子属性的对象

export default

在 ES6 模块系统中,使用 export default 可以导出一个默认值,使用方可以用 import foo from 'foo' 而不是 import { foo } from 'foo' 来导入这个默认值。

在类型声明文件中,export default 用来导出默认值的类型:

注意,只有 functionclassinterface 可以直接默认导出,其他的变量需要先定义出来,再默认导出19

上例中 export default enum 是错误的语法,需要使用 declare enum 定义出来,然后使用 export default 导出:

针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面

export =

commonjs 规范中,我们用以下方式来导出一个模块:

在 TypeScript 中,针对这种模块导出,有多种方式可以导入,第一种方式是 const ... = require

第二种方式是 import ... from,注意针对整体导出,需要使用 import * as 来导入:

第三种方式是 import ... require,这也是 TypeScript 官方推荐的方式:

对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 export = 这种语法

需要注意的是,上例中使用了 export = 之后,就不能再单个导出 export { bar } 了。所以我们通过声明合并,使用 declare namespace foo 来将 bar 合并到 foo 里。

准确地讲,export = 不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,import ... requireexport = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档

由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 export = 这种语法了。但是还是需要再强调下,相比与 export =,我们更推荐使用 ES6 标准的 export defaultexport

UMD 库

UMD 库既可以通过 <script> 标签引入,又可以通过 import 导入。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,TypeScript 提供了一个新语法export as namespace,即可将声明好的一个变量声明为全局变量。

当然也可以与 export default 一起使用:

直接扩展全局变量

对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后扩展全局变量,则需要使用另一种语法 declare global

也可以使用 declare namespace 给已有的命名空间添加类型声明

模块类型

有时通过import导入一个模块时,我们可能想在其上绑定一个额外的属性类型。TypeScript 提供了一个语法declare module,它可以同来扩展原有模块的类型。

declare module也可以用在一个文件中一次性声明多个模块的类型:

声明文件中的依赖

一个声明文件有时会依赖另一个声明文件中的类型,例如:

除了在声明文件中通过 import导入另一个声明文件中的类型之外,还有一个语法也可以导入另一个声明文件,那就是三斜线指令。

三斜线指令也是 TypeScript 在早期版本中为了描述模块之间依赖关系而创造的语句。随着 ES6 的广泛应用,现在已经不建议使用它来描述模块之间的依赖关系了。

使用三斜线指令我们可以导入另一个声明文件,与import的区别是,当且仅当在以下几个场景中,我们才需要使用三斜线指令替代 import

  • 当我们在书写一个全局变量的声明文件时。

  • 当我们需要依赖一个全局变量的声明文件时。

在全局变量的声明文件中,是不允许出现 importexport 关键字的,一旦出现,那么它就会背视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。因此当我们在书写一个全局变量的声明文件时,如果需要另一个库的类型,那么就必须使用三斜线指令。

在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了

由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。

拆分声明文件

当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们引入,来提高代码的可维护性。

其中用到了 typespath 两种不同的指令。它们的区别是:types 用于声明对另一个库的依赖,而 path 用于声明对另一个文件的依赖。

上例中,sizzle 是与 jquery 平行的另一个库,所以需要使用 types="sizzle" 来声明对它的依赖。而其他的三斜线指令就是将 jquery 的声明拆分到不同的文件中了,然后在这个入口文件中使用 path="foo" 将它们引入。

除了这两种三斜线指令之外,还有其他的三斜线指令,比如 /// <reference no-default-lib="true"/>, /// <amd-module /> 等,但它们都是废弃的语法,故这里就不介绍了,详情可见官网

这有帮助吗?