编写声明文件
当一个第三方库没有提供声明文件时,这时我们就需要自己去编写声明文件。在不同的场景下,声明文件的内容和使用方式会有所不同。下满我们会逐一介绍到。
全局变量
全局变量的类型声明是最简单的一种场景,前面我们介绍到的 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 现不再推荐使用,但在声明文件中还是比较常用的,它通常用来表示全局变量是一个对象,包含很多字属性。
在declare namespace中,我们可以直接使用 function foo 来声明一个函数,而不是使用 declare function foo。类似的,也可以直接使用 const、class 等语法,来声明一个常量、类等。
嵌套的命名空间
如果一个对象拥有多个属性,并且每个属性又是一个对象,我们可以通过嵌套 namespace 来表示。
interface 和 type
在声明文件中,除了全局变量之外,还会有一些其它类型也希望暴露出来,在类型声明文件中,我们可以通过使用 interface 或 type 关键字来声明一个全局接口或类型。
暴露在最外层的 interface 或 type 会作为全局类型作用于整个项目,我们应该尽可能的减少全局变量或类型的数量。最好将他们集中在 namespace下。
声明合并
假如 jQuery 既是一个函数,又是一个拥有 ajax 方法的对象,那么我们可以组合多个声明语句,它们会不冲突的合并起来。
npm 包
在项目中我们通过 import axios from 'axios' 导入一个 npm 包,这是符合 ES6 模块规范的。在我们尝试给一个第三方模块创建声明文件之前,需要查看一下它的声明文件是否已经提供了。
一般来说,npm 包的声明文件可能存在于两个地方:
声明文件与该包绑定在一起。判断依据是
package.json中有types字段,或者有一个index.d.ts声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。发布到
@types里。我们只需要尝试安装一下对应的@types包就知道是否存在该声明文件,安装命令是npm install @types/axios --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到@types里了。
假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 import 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案:
创建一个
node_modules/@types/axios/index.d.ts文件,存放axios模块的声明文件。这种方式不需要额外的配置,但是node_modules目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。创建一个
types目录,专门用来管理自己写的声明文件,将axios的声明文件放到types/axios/index.d.ts中。这种方式需要配置下tsconfig.json中的paths和baseUrl字段。
配置好之后,通过 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 用来导出默认值的类型:
注意,只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出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 ... require 和 export = 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看官方文档。
由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 export = 这种语法了。但是还是需要再强调下,相比与 export =,我们更推荐使用 ES6 标准的 export default 和 export。
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:
当我们在书写一个全局变量的声明文件时。
当我们需要依赖一个全局变量的声明文件时。
在全局变量的声明文件中,是不允许出现 import、export 关键字的,一旦出现,那么它就会背视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。因此当我们在书写一个全局变量的声明文件时,如果需要另一个库的类型,那么就必须使用三斜线指令。
三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了
由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。
拆分声明文件
当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们引入,来提高代码的可维护性。
其中用到了 types 和 path 两种不同的指令。它们的区别是:types 用于声明对另一个库的依赖,而 path 用于声明对另一个文件的依赖。
上例中,sizzle 是与 jquery 平行的另一个库,所以需要使用 types="sizzle" 来声明对它的依赖。而其他的三斜线指令就是将 jquery 的声明拆分到不同的文件中了,然后在这个入口文件中使用 path="foo" 将它们引入。
除了这两种三斜线指令之外,还有其他的三斜线指令,比如 /// <reference no-default-lib="true"/>, /// <amd-module /> 等,但它们都是废弃的语法,故这里就不介绍了,详情可见官网。
这有帮助吗?