💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 声明文件 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。 ## 新语法索引 由于本章涉及大量新语法,故在本章开头列出新语法的索引,方便大家在使用这些新语法时能快速查找到对应的讲解: - [`declare var`](#declare-var) 声明全局变量 - [`declare function`](#declare-function) 声明全局方法 - [`declare class`](#declare-class) 声明全局类 - [`declare enum`](#declare-enum) 声明全局枚举类型 - [`declare namespace`](#declare-namespace) 声明(含有子属性的)全局对象 - [`interface` 和 `type`](#interface-he-type) 声明全局类型 - [`export`](#export) 导出变量 - [`export namespace`](#export-namespace) 导出(含有子属性的)对象 - [`export default`](#export-default) ES6 默认导出 - [`export =`](#export-1) commonjs 导出模块 - [`export as namespace`](#export-as-namespace) UMD 库声明全局变量 - [`declare global`](#declare-global) 扩展全局变量 - [`declare module`](#declare-module) 扩展模块 - [`/// <reference />`](#san-xie-xian-zhi-ling) 三斜线指令 ## 什么是声明语句 假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 `<script>` 标签引入 jQuery,然后就可以使用全局变量 `$` 或 `jQuery` 了。 我们通常这样获取一个 `id` 是 `foo` 的元素: ```js $('#foo'); // or jQuery('#foo'); ``` 但是在 ts 中,编译器并不知道 `$` 或 `jQuery` 是什么东西[<sup>1</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/01-jquery): ```ts jQuery('#foo'); // ERROR: Cannot find name 'jQuery'. ``` 这时,我们需要使用 `declare var` 来定义它的类型[<sup>2</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/02-declare-var): ```ts declare var jQuery: (selector: string) => any; jQuery('#foo'); ``` 上例中,`declare var` 并没有真的定义一个变量,只是定义了全局变量 `jQuery` 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是: ```js jQuery('#foo'); ``` 除了 `declare var` 之外,还有其他很多种声明语句,将会在后面详细介绍。 ## 什么是声明文件 通常我们会把声明语句放到一个单独的文件(`jQuery.d.ts`)中,这就是声明文件[<sup>3</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/03-jquery-d-ts): ```ts // src/jQuery.d.ts declare var jQuery: (selector: string) => any; ``` ```ts // src/index.ts jQuery('#foo'); ``` 声明文件必需以 `.d.ts` 为后缀。 一般来说,ts 会解析项目中所有的 `*.ts` 文件,当然也包含以 `.d.ts` 结尾的文件。所以当我们将 `jQuery.d.ts` 放到项目中时,其他所有 `*.ts` 文件就都可以获得 `jQuery` 的类型定义了。 ```plain /path/to/project ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json ``` 假如仍然无法解析,那么可以检查下 `tsconfig.json` 中的 `files`、`include` 和 `exclude` 配置,确保其包含了 `jQuery.d.ts` 文件。 这里只演示了全局变量这种模式的声明文件,假如是通过模块导入的方式使用第三方库的话,那么引入声明文件又是另一种方式了,将会在后面详细介绍。 ### 第三方声明文件 当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:[jQuery in DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jquery/index.d.ts)。 我们可以直接下载下来使用,但是更推荐的是使用 `@types` 统一管理第三方库的声明文件。 `@types` 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例: ```bash npm install @types/jquery --save-dev ``` 可以在[这个页面](https://microsoft.github.io/TypeSearch/)搜索你需要的声明文件。 ## 书写声明文件 当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了。前面只介绍了最简单的声明文件内容,而真正书写一个声明文件并不是一件简单的事,以下会详细介绍如何书写声明文件。 在不同的场景下,声明文件的内容和使用方式会有所区别。 库的使用场景主要有以下几种: - [全局变量](#quan-ju-bian-liang):通过 `<script>` 标签引入第三方库,注入全局变量 - [npm 包](#npm-bao):通过 `import foo from 'foo'` 导入,符合 ES6 模块规范 - [UMD 库](#umd-ku):既可以通过 `<script>` 标签引入,又可以通过 `import` 导入 - [直接扩展全局变量](#zhi-jie-kuo-zhan-quan-ju-bian-liang):通过 `<script>` 标签引入后,改变一个全局变量的结构 - [在 npm 包或 UMD 库中扩展全局变量](#zai-npm-bao-huo-umd-ku-zhong-kuo-zhan-quan-ju-bian-liang):引用 npm 包或 UMD 库后,改变一个全局变量的结构 - [模块插件](#mo-kuai-cha-jian):通过 `<script>` 或 `import` 导入后,改变另一个模块的结构 ### 全局变量 全局变量是最简单的一种场景,之前举的例子就是通过 `<script>` 标签引入 jQuery,注入全局变量 `$` 和 `jQuery`。 使用全局变量的声明文件时,如果是以 `npm install @types/xxx --save-dev` 安装的,则不需要任何配置。如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到 `src` 目录下(或者对应的源码目录下): ```plain /path/to/project ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json ``` 如果没有生效,可以检查下 `tsconfig.json` 中的 `files`、`include` 和 `exclude` 配置,确保其包含了 `jQuery.d.ts` 文件。 全局变量的声明文件主要有以下几种语法: - [`declare var`](#declare-var) 声明全局变量 - [`declare function`](#declare-function) 声明全局方法 - [`declare class`](#declare-class) 声明全局类 - [`declare enum`](#declare-enum) 声明全局枚举类型 - [`declare namespace`](#declare-namespace) 声明(含有子属性的)全局对象 - [`interface` 和 `type`](#interface-he-type) 声明全局类型 #### `declare var` 在所有的声明语句中,`declare var` 是最简单的,如之前所学,它能够用来定义一个全局变量的类型。与其类似的,还有 `declare let` 和 `declare const`,使用 `let` 与使用 `var` 没有什么区别: ```ts // src/jQuery.d.ts declare let jQuery: (selector: string) => any; ``` ```ts // src/index.ts jQuery('#foo'); // 使用 declare let 定义的 jQuery 类型,允许修改这个全局变量 jQuery = function(selector) { return document.querySelector(selector); }; ``` 而当我们使用 `const` 定义时,表示此时的全局变量是一个常量,不允许再去修改它的值了[<sup>4</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/04-declare-const-jquery): ```ts // src/jQuery.d.ts declare const jQuery: (selector: string) => any; jQuery('#foo'); // 使用 declare const 定义的 jQuery 类型,禁止修改这个全局变量 jQuery = function(selector) { return document.querySelector(selector); }; // ERROR: Cannot assign to 'jQuery' because it is a constant or a read-only property. ``` 一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用 `const` 而不是 `var` 或 `let`。 需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现[<sup>5</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/05-declare-jquery-value): ```ts declare const jQuery = function(selector) { return document.querySelector(selector); }; // ERROR: An implementation cannot be declared in ambient contexts. ``` #### `declare function` `declare function` 用来定义全局函数的类型。jQuery 其实就是一个函数,所以也可以用 `function` 来定义: ```ts // src/jQuery.d.ts declare function jQuery(selector: string): any; ``` ```ts // src/index.ts jQuery('#foo'); ``` 在函数类型的声明语句中,函数重载也是支持的[<sup>6</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/06-declare-function): ```ts // src/jQuery.d.ts declare function jQuery(selector: string): any; declare function jQuery(domReadyCallback: () => any): any; ``` ```ts // src/index.ts jQuery('#foo'); jQuery(function() { alert('Dom Ready!'); }); ``` #### `declare class` 当全局变量是一个类的时候,我们用 `declare class` 来定义它的类型[<sup>7</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/07-declare-class): ```ts // src/Animal.d.ts declare class Animal { name: string; constructor(name: string); sayHi(): string; } ``` ```ts // src/index.ts let cat = new Animal('Tom'); ``` 同样的,`declare class` 语句也只能用来定义类型,不能用来定义具体的实现,比如定义 `sayHi` 方法的具体实现则会报错: ```ts // src/Animal.d.ts declare class Animal { name: string; constructor(name: string); sayHi() { return `My name is ${this.name}`; }; // ERROR: An implementation cannot be declared in ambient contexts. } ``` #### `declare enum` 使用 `declare enum` 定义的枚举类型也称作外部枚举(Ambient Enums),举例如下[<sup>8</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/08-declare-enum): ```ts // src/Directions.d.ts declare enum Directions { Up, Down, Left, Right } ``` ```ts // src/index.ts let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; ``` 与其他全局变量的类型声明一致,`declare enum` 仅用来定义类型,而不是具体的值。 `Directions.d.ts` 仅仅会用于编译时的检查,声明文件里的内容在编译结果中会被删除。它编译结果是: ```js var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; ``` 其中 `Directions` 是由第三方库定义好的全局变量。 #### `declare namespace` `namespace` 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。 由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 `module` 关键字表示内部模块。但由于后来 ES6 也使用了 `module` 关键字,ts 为了兼容 ES6,使用 `namespace` 替代了自己的 `module`,更名为命名空间。 随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 `namespace`,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 `namespace` 的使用了。 `namespace` 被淘汰了,但是在声明文件中,`declare namespace` 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。 比如 `jQuery` 是一个全局变量,它是一个对象,提供了一个 `jQuery.ajax` 方法可以调用,那么我们就应该使用 `declare namespace jQuery` 来声明这个拥有多个子属性的全局变量。 ```ts // src/jQuery.d.ts declare namespace jQuery { function ajax(url: string, settings?: any): void; } ``` ```ts // src/index.ts jQuery.ajax('/api/get_something'); ``` 注意,在 `declare namespace` 内部,我们直接使用 `function ajax` 来声明函数,而不是使用 `declare function ajax`。类似的,也可以使用 `const`, `class`, `enum` 等语句[<sup>9</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/09-declare-namespace): ```ts // src/jQuery.d.ts declare namespace jQuery { function ajax(url: string, settings?: any): void; const version: number; class Event { blur(eventType: EventType): void } enum EventType { CustomClick } } ``` ```ts // src/index.ts jQuery.ajax('/api/get_something'); console.log(jQuery.version); const e = new jQuery.Event(); e.blur(jQuery.EventType.CustomClick); ``` ##### 嵌套的命名空间 如果对象拥有深层的层级,则需要用嵌套的 `namespace` 来声明深层的属性的类型[<sup>10</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/10-declare-namespace-nesting): ```ts // src/jQuery.d.ts declare namespace jQuery { function ajax(url: string, settings?: any): void; namespace fn { function extend(object: any): void; } } ``` ```ts // src/index.ts jQuery.ajax('/api/get_something'); jQuery.fn.extend({ check: function() { return this.each(function() { this.checked = true; }); } }); ``` 假如 `jQuery` 下仅有 `fn` 这一个属性(没有 `ajax` 等其他属性或方法),则可以不需要嵌套 `namespace`[<sup>11</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/11-declare-namespace-dot): ```ts // src/jQuery.d.ts declare namespace jQuery.fn { function extend(object: any): void; } ``` ```ts // src/index.ts jQuery.fn.extend({ check: function() { return this.each(function() { this.checked = true; }); } }); ``` #### `interface` 和 `type` 除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 `interface` 或 `type` 来声明一个全局的接口或类型[<sup>12</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/12-interface): ```ts // src/jQuery.d.ts interface AjaxSettings { method?: 'GET' | 'POST' data?: any; } declare namespace jQuery { function ajax(url: string, settings?: AjaxSettings): void; } ``` 这样的话,在其他文件中也可以使用这个接口或类型了: ```ts // src/index.ts let settings: AjaxSettings = { method: 'POST', data: { name: 'foo' } }; jQuery.ajax('/api/post_something', settings); ``` `type` 与 `interface` 类似,不再赘述。 ##### 防止命名冲突 暴露在最外层的 `interface` 或 `type` 会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到 `namespace` 下[<sup>13</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/13-avoid-name-conflict): ```ts // src/jQuery.d.ts declare namespace jQuery { interface AjaxSettings { method?: 'GET' | 'POST' data?: any; } function ajax(url: string, settings?: AjaxSettings): void; } ``` 注意,在使用这个 `interface` 的时候,也应该加上 `jQuery` 前缀: ```ts // src/index.ts let settings: jQuery.AjaxSettings = { method: 'POST', data: { name: 'foo' } }; jQuery.ajax('/api/post_something', settings); ``` #### 声明合并 假如 jQuery 既是一个函数,可以直接被调用 `jQuery('#foo')`,又是一个对象,拥有子属性 `jQuery.ajax()`(事实确实如此),那么我们可以组合多个声明语句,它们会不冲突的合并起来[<sup>14</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/14-declaration-merging): ```ts // src/jQuery.d.ts declare function jQuery(selector: string): any; declare namespace jQuery { function ajax(url: string, settings?: any): void; } ``` ```ts // src/index.ts jQuery('#foo'); jQuery.ajax('/api/get_something'); ``` 关于声明合并的更多用法,可以查看[声明合并](../advanced/declaration-merging.md)章节。 ### npm 包 一般我们通过 `import foo from 'foo'` 导入一个 npm 包,这是符合 ES6 模块规范的。 在我们尝试给一个 npm 包创建声明文件之前,需要先看看它的声明文件是否已经存在。一般来说,npm 包的声明文件可能存在于两个地方: 1. 与该 npm 包绑定在一起。判断依据是 `package.json` 中有 `types` 字段,或者有一个 `index.d.ts` 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。 2. 发布到 `@types` 里。我们只需要尝试安装一下对应的 `@types` 包就知道是否存在该声明文件,安装命令是 `npm install @types/foo --save-dev`。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 `@types` 里了。 假如以上两种方式都没有找到对应的声明文件,那么我们就需要自己为它写声明文件了。由于是通过 `import` 语句导入的模块,所以声明文件存放的位置也有所约束,一般有两种方案: 1. 创建一个 `node_modules/@types/foo/index.d.ts` 文件,存放 `foo` 模块的声明文件。这种方式不需要额外的配置,但是 `node_modules` 目录不稳定,代码也没有被保存到仓库中,无法回溯版本,有不小心被删除的风险,故不太建议用这种方案,一般只用作临时测试。 2. 创建一个 `types` 目录,专门用来管理自己写的声明文件,将 `foo` 的声明文件放到 `types/foo/index.d.ts` 中。这种方式需要配置下 `tsconfig.json` 中的 `paths` 和 `baseUrl` 字段。 目录结构: ```plain /path/to/project ├── src | └── index.ts ├── types | └── foo | └── index.d.ts └── tsconfig.json ``` `tsconfig.json` 内容: ```json { "compilerOptions": { "module": "commonjs", "baseUrl": "./", "paths": { "*": ["types/*"] } } } ``` 如此配置之后,通过 `import` 导入 `foo` 的时候,也会去 `types` 目录下寻找对应的模块的声明文件了。 注意 `module` 配置可以有很多种选项,不同的选项会影响模块的导入导出模式。这里我们使用了 `commonjs` 这个最常用的选项,后面的教程也都默认使用的这个选项。 不管采用了以上两种方式中的哪一种,我都**强烈建议**大家将书写好的声明文件(通过给第三方库发 pull request,或者直接提交到 `@types` 里)发布到开源社区中,享受了这么多社区的优秀的资源,就应该在力所能及的时候给出一些回馈。只有所有人都参与进来,才能让 ts 社区更加繁荣。 npm 包的声明文件主要有以下几种语法: - [`export`](#export) 导出变量 - [`export namespace`](#export-namespace) 导出(含有子属性的)对象 - [`export default`](#export-default) ES6 默认导出 - [`export =`](#export-1) commonjs 导出模块 #### `export` npm 包的声明文件与全局变量的声明文件有很大区别。在 npm 包的声明文件中,使用 `declare` 不再会声明一个全局变量,而只会在当前文件中声明一个局部变量。只有在声明文件中使用 `export` 导出,然后在使用方 `import` 导入后,才会应用到这些类型声明。 `export` 的语法与普通的 ts 中的语法类似,区别仅在于声明文件中禁止定义具体的实现[<sup>15</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/15-export): ```ts // types/foo/index.d.ts export const name: string; export function getName(): string; export class Animal { constructor(name: string); sayHi(): string; } export enum Directions { Up, Down, Left, Right } export interface Options { data: any; } ``` 对应的导入和使用模块应该是这样: ```ts // src/index.ts import { name, getName, Animal, Directions, Options } from 'foo'; console.log(name); let myName = getName(); let cat = new Animal('Tom'); let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]; let options: Options = { data: { name: 'foo' } }; ``` ##### 混用 `declare` 和 `export` 我们也可以使用 `declare` 先声明多个变量,最后再用 `export` 一次性导出。上例的声明文件可以等价的改写为[<sup>16</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/16-declare-and-export): ```ts // types/foo/index.d.ts declare const name: string; declare function getName(): string; declare class Animal { constructor(name: string); sayHi(): string; } declare enum Directions { Up, Down, Left, Right } interface Options { data: any; } export { name, getName, Animal, Directions, Options }; ``` 注意,与全局变量的声明文件类似,`interface` 前是不需要 `declare` 的。 #### `export namespace` 与 `declare namespace` 类似,`export namespace` 用来导出一个拥有子属性的对象[<sup>17</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/17-export-namespace): ```ts // types/foo/index.d.ts export namespace foo { const name: string; namespace bar { function baz(): string; } } ``` ```ts // src/index.ts import { foo } from 'foo'; console.log(foo.name); foo.bar.baz(); ``` #### `export default` 在 ES6 模块系统中,使用 `export default` 可以导出一个默认值,使用方可以用 `import foo from 'foo'` 而不是 `import { foo } from 'foo'` 来导入这个默认值。 在类型声明文件中,`export default` 用来导出默认值的类型[<sup>18</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/18-export-default): ```ts // types/foo/index.d.ts export default function foo(): string; ``` ```ts // src/index.ts import foo from 'foo'; foo(); ``` 注意,只有 `function`、`class` 和 `interface` 可以直接默认导出,其他的变量需要先定义出来,再默认导出[<sup>19</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/19-export-default-enum-error): ```ts // types/foo/index.d.ts export default enum Directions { // ERROR: Expression expected. Up, Down, Left, Right } ``` 上例中 `export default enum` 是错误的语法,需要使用 `declare enum` 定义出来,然后使用 `export default` 导出: ```ts // types/foo/index.d.ts declare enum Directions { Up, Down, Left, Right } export default Directions; ``` 针对这种默认导出,我们一般会将导出语句放在整个声明文件的最前面[<sup>20</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/20-export-default-enum): ```ts // types/foo/index.d.ts export default Directions; declare enum Directions { Up, Down, Left, Right } ``` #### `export =` 在 commonjs 规范中,我们用以下方式来导出一个模块: ```js // 整体导出 module.exports = foo; // 单个导出 exports.bar = bar; ``` 在 ts 中,针对这种模块导出,有多种方式可以导入,第一种方式是 `const ... = require`: ```js // 整体导入 const foo = require('foo'); // 单个导入 const bar = require('foo').bar; ``` 第二种方式是 `import ... from`,注意针对整体导出,需要使用 `import * as` 来导入: ```ts // 整体导入 import * as foo from 'foo'; // 单个导入 import { bar } from 'foo'; ``` 第三种方式是 `import ... require`,这也是 ts 官方推荐的方式: ```ts // 整体导入 import foo = require('foo'); // 单个导入 import bar = foo.bar; ``` 对于这种使用 commonjs 规范的库,假如要为它写类型声明文件的话,就需要使用到 `export =` 这种语法了[<sup>21</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/21-export-equal): ```ts // types/foo/index.d.ts export = foo; declare function foo(): string; declare namespace foo { const bar: number; } ``` 需要注意的是,上例中使用了 `export =` 之后,就不能再单个导出 `export { bar }` 了。所以我们通过声明合并,使用 `declare namespace foo` 来将 `bar` 合并到 `foo` 里。 准确地讲,`export =` 不仅可以用在声明文件中,也可以用在普通的 ts 文件中。实际上,`import ... require` 和 `export =` 都是 ts 为了兼容 AMD 规范和 commonjs 规范而创立的新语法,由于并不常用也不推荐使用,所以这里就不详细介绍了,感兴趣的可以看[官方文档](https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require)。 由于很多第三方库是 commonjs 规范的,所以声明文件也就不得不用到 `export =` 这种语法了。但是还是需要再强调下,相比与 `export =`,我们更推荐使用 ES6 标准的 `export default` 和 `export`。 ### UMD 库 既可以通过 `<script>` 标签引入,又可以通过 `import` 导入的库,称为 UMD 库。相比于 npm 包的类型声明文件,我们需要额外声明一个全局变量,为了实现这种方式,ts 提供了一个新语法 `export as namespace`。 #### `export as namespace` 一般使用 `export as namespace` 时,都是先有了 npm 包的声明文件,再基于它添加一条 `export as namespace` 语句,即可将声明好的一个变量声明为全局变量,举例如下[<sup>22</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/22-export-as-namespace): ```ts // types/foo/index.d.ts export as namespace foo; export = foo; declare function foo(): string; declare namespace foo { const bar: number; } ``` 当然它也可以与 `export default` 一起使用: ```ts // types/foo/index.d.ts export as namespace foo; export default foo; declare function foo(): string; declare namespace foo { const bar: number; } ``` ### 直接扩展全局变量 有的第三方库扩展了一个全局变量,可是此全局变量的类型却没有相应的更新过来,就会导致 ts 编译错误,此时就需要扩展全局变量的类型。比如扩展 `String` 类型[<sup>23</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/23-merge-global-interface): ```ts interface String { prependHello(): string; } 'foo'.prependHello(); ``` 通过声明合并,使用 `interface String` 即可给 `String` 添加属性或方法。 也可以使用 `declare namespace` 给已有的命名空间添加类型声明[<sup>24</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/24-merge-global-namespace): ```ts // types/jquery-plugin/index.d.ts declare namespace JQuery { interface CustomOptions { bar: string; } } interface JQueryStatic { foo(options: JQuery.CustomOptions): string; } ``` ```ts // src/index.ts jQuery.foo({ bar: '' }); ``` ### 在 npm 包或 UMD 库中扩展全局变量 如之前所说,对于一个 npm 包或者 UMD 库的声明文件,只有 `export` 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 `declare global`。 #### `declare global` 使用 `declare global` 可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型[<sup>25</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/25-declare-global): ```ts // types/foo/index.d.ts declare global { interface String { prependHello(): string; } } export {}; ``` ```ts // src/index.ts 'bar'.prependHello(); ``` 注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。 ### 模块插件 有时通过 `import` 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 `declare module`,它可以用来扩展原有模块的类型。 #### `declare module` 如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 `declare module` 扩展原有模块[<sup>26</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/26-declare-module): ```ts // types/moment-plugin/index.d.ts import * as moment from 'moment'; declare module 'moment' { export function foo(): moment.CalendarKey; } ``` ```ts // src/index.ts import * as moment from 'moment'; import 'moment-plugin'; moment.foo(); ``` `declare module` 也可用于在一个文件中一次性声明多个模块的类型[<sup>27</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/27-multiple-declare-module): ```ts // types/foo-bar.d.ts declare module 'foo' { export interface Foo { foo: string; } } declare module 'bar' { export function bar(): string; } ``` ```ts // src/index.ts import { Foo } from 'foo'; import * as bar from 'bar'; let f: Foo; bar.bar(); ``` ### 声明文件中的依赖 一个声明文件有时会依赖另一个声明文件中的类型,比如在前面的 `declare module` 的例子中,我们就在声明文件中导入了 `moment`,并且使用了 `moment.CalendarKey` 这个类型: ```ts // types/moment-plugin/index.d.ts import * as moment from 'moment'; declare module 'moment' { export function foo(): moment.CalendarKey; } ``` 除了可以在声明文件中通过 `import` 导入另一个声明文件中的类型之外,还有一个语法也可以用来导入另一个声明文件,那就是三斜线指令。 #### 三斜线指令 与 `namespace` 类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。 但是在声明文件中,它还是有一定的用武之地。 类似于声明文件中的 `import`,它可以用来导入另一个声明文件。与 `import` 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 `import`: - 当我们在**书写**一个全局变量的声明文件时 - 当我们需要**依赖**一个全局变量的声明文件时 ##### **书写**一个全局变量的声明文件 这些场景听上去很拗口,但实际上很好理解——在全局变量的声明文件中,是不允许出现 `import`, `export` 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了[<sup>28</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/28-triple-slash-directives): ```ts // types/jquery-plugin/index.d.ts /// <reference types="jquery" /> declare function foo(options: JQuery.AjaxSettings): string; ``` ```ts // src/index.ts foo({}); ``` 三斜线指令的语法如上,`///` 后面使用 xml 的格式添加了对 `jquery` 类型的依赖,这样就可以在声明文件中使用 `JQuery.AjaxSettings` 类型了。 注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。 ##### **依赖**一个全局变量的声明文件 在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 `import` 导入,当然也就必须使用三斜线指令来引入了[<sup>29</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/29-triple-slash-directives-global): ```ts // types/node-plugin/index.d.ts /// <reference types="node" /> export function foo(p: NodeJS.Process): string; ``` ```ts // src/index.ts import { foo } from 'node-plugin'; foo(global.process); ``` 在上面的例子中,我们通过三斜线指引入了 `node` 的类型,然后在声明文件中使用了 `NodeJS.Process` 这个类型。最后在使用到 `foo` 的时候,传入了 `node` 中的全局变量 `process`。 由于引入的 `node` 中的类型都是全局变量的类型,它们是没有办法通过 `import` 来导入的,所以这种场景下也只能通过三斜线指令来引入了。 以上两种使用场景下,都是由于需要书写或需要依赖全局变量的声明文件,所以必须使用三斜线指令。在其他的一些不是必要使用三斜线指令的情况下,就都需要使用 `import` 来导入。 ##### 拆分声明文件 当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。比如 `jQuery` 的声明文件就是这样的: ```ts // node_modules/@types/jquery/index.d.ts /// <reference types="sizzle" /> /// <reference path="JQueryStatic.d.ts" /> /// <reference path="JQuery.d.ts" /> /// <reference path="misc.d.ts" /> /// <reference path="legacy.d.ts" /> export = jQuery; ``` 其中用到了 `types` 和 `path` 两种不同的指令。它们的区别是:`types` 用于声明对另一个库的依赖,而 `path` 用于声明对另一个文件的依赖。 上例中,`sizzle` 是与 `jquery` 平行的另一个库,所以需要使用 `types="sizzle"` 来声明对它的依赖。而其他的三斜线指令就是将 `jquery` 的声明拆分到不同的文件中了,然后在这个入口文件中使用 `path="foo"` 将它们一一引入。 ##### 其他三斜线指令 除了这两种三斜线指令之外,还有其他的三斜线指令,比如 `/// <reference no-default-lib="true"/>`, `/// <amd-module />` 等,但它们都是废弃的语法,故这里就不介绍了,详情可见[官网](http://www.typescriptlang.org/docs/handbook/triple-slash-directives.html)。 ### 自动生成声明文件 如果库的源码本身就是由 ts 写的,那么在使用 `tsc` 脚本将 ts 编译为 js 的时候,添加 `declaration` 选项,就可以同时也生成 `.d.ts` 声明文件了。 我们可以在命令行中添加 `--declaration`(简写 `-d`),或者在 `tsconfig.json` 中添加 `declaration` 选项。这里以 `tsconfig.json` 为例: ```json { "compilerOptions": { "module": "commonjs", "outDir": "lib", "declaration": true, } } ``` 上例中我们添加了 `outDir` 选项,将 ts 文件的编译结果输出到 `lib` 目录下,然后添加了 `declaration` 选项,设置为 `true`,表示将会由 ts 文件自动生成 `.d.ts` 声明文件,也会输出到 `lib` 目录下。 运行 `tsc` 之后,目录结构如下[<sup>30</sup>](https://github.com/xcatliu/typescript-tutorial/tree/master/examples/declaration-files/30-auto-d-ts): ```plain /path/to/project ├── lib | ├── bar | | ├── index.d.ts | | └── index.js | ├── index.d.ts | └── index.js ├── src | ├── bar | | └── index.ts | └── index.ts ├── package.json └── tsconfig.json ``` 在这个例子中,`src` 目录下有两个 ts 文件,分别是 `src/index.ts` 和 `src/bar/index.ts`,它们被编译到 `lib` 目录下的同时,也会生成对应的两个声明文件 `lib/index.d.ts` 和 `lib/bar/index.d.ts`。它们的内容分别是: ```ts // src/index.ts export * from './bar'; export default function foo() { return 'foo'; } ``` ```ts // src/bar/index.ts export function bar() { return 'bar'; } ``` ```ts // lib/index.d.ts export * from './bar'; export default function foo(): string; ``` ```ts // lib/bar/index.d.ts export declare function bar(): string; ``` 可见,自动生成的声明文件基本保持了源码的结构,而将具体实现去掉了,生成了对应的类型声明。 使用 `tsc` 自动生成声明文件时,每个 ts 文件都会对应一个 `.d.ts` 声明文件。这样的好处是,使用方不仅可以在使用 `import foo from 'foo'` 导入默认的模块时获得类型提示,还可以在使用 `import bar from 'foo/lib/bar'` 导入一个子模块时,也获得对应的类型提示。 除了 `declaration` 选项之外,还有几个选项也与自动生成声明文件有关,这里只简单列举出来,不做详细演示了: - `declarationDir` 设置生成 `.d.ts` 文件的目录 - `declarationMap` 对每个 `.d.ts` 文件,都生成对应的 `.d.ts.map`(sourcemap)文件 - `emitDeclarationOnly` 仅生成 `.d.ts` 文件,不生成 `.js` 文件 ## 发布声明文件 当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。 此时有两种方案: 1. 将声明文件和源码放在一起 2. 将声明文件发布到 `@types` 下 这两种方案中优先选择第一种方案。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。 仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到 `@types` 下。 ### 将声明文件和源码放在一起 如果声明文件是通过 `tsc` 自动生成的,那么无需做任何其他配置,只需要把编译好的文件也发布到 npm 上,使用方就可以获取到类型提示了。 如果是手动写的声明文件,那么需要满足以下条件之一,才能被正确的识别: - 给 `package.json` 中的 `types` 或 `typings` 字段指定一个类型声明文件地址 - 在项目根目录下,编写一个 `index.d.ts` 文件 - 针对入口文件(`package.json` 中的 `main` 字段指定的入口文件),编写一个同名不同后缀的 `.d.ts` 文件 第一种方式是给 `package.json` 中的 `types` 或 `typings` 字段指定一个类型声明文件地址。比如: ```json { "name": "foo", "version": "1.0.0", "main": "lib/index.js", "types": "foo.d.ts", } ``` 指定了 `types` 为 `foo.d.ts` 之后,导入此库的时候,就会去找 `foo.d.ts` 作为此库的类型声明文件了。 `typings` 与 `types` 一样,只是另一种写法。 如果没有指定 `types` 或 `typings`,那么就会在根目录下寻找 `index.d.ts` 文件,将它视为此库的类型声明文件。 如果没有找到 `index.d.ts` 文件,那么就会寻找入口文件(`package.json` 中的 `main` 字段指定的入口文件)是否存在对应同名不同后缀的 `.d.ts` 文件。 比如 `package.json` 是这样时: ```json { "name": "foo", "version": "1.0.0", "main": "lib/index.js" } ``` 就会先识别 `package.json` 中是否存在 `types` 或 `typings` 字段。发现不存在,那么就会寻找是否存在 `index.d.ts` 文件。如果还是不存在,那么就会寻找是否存在 `lib/index.d.ts` 文件。假如说连 `lib/index.d.ts` 都不存在的话,就会被认为是一个没有提供类型声明文件的库了。 有的库为了支持导入子模块,比如 `import bar from 'foo/lib/bar'`,就需要额外再编写一个类型声明文件 `lib/bar.d.ts` 或者 `lib/bar/index.d.ts`,这与自动生成声明文件类似,一个库中同时包含了多个类型声明文件。 ### 将声明文件发布到 `@types` 下 如果我们是在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request,那么就需要将声明文件发布到 `@types` 下。 与普通的 npm 模块不同,`@types` 是统一由 [DefinitelyTyped][] 管理的。要将声明文件发布到 `@types` 下,就需要给 [DefinitelyTyped][] 创建一个 pull-request,其中包含了类型声明文件,测试代码,以及 `tsconfig.json` 等。 pull-request 需要符合它们的规范,并且通过测试,才能被合并,稍后就会被自动发布到 `@types` 下。 在 [DefinitelyTyped][] 中创建一个新的类型声明,需要用到一些工具,[DefinitelyTyped][] 的文档中已经有了[详细的介绍](https://github.com/DefinitelyTyped/DefinitelyTyped#create-a-new-package),这里就不赘述了,以官方文档为准。 如果大家有此类需求,可以参考下笔者[提交的 pull-request](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/30336/files) 。 ## 参考 - [Writing Declaration Files](http://www.typescriptlang.org/docs/handbook/writing-declaration-files.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/declaration%20files/Introduction.html)) - [Triple-Slash Directives](http://www.typescriptlang.org/docs/handbook/triple-slash-directives.html)([中文版](https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Triple-Slash%20Directives.html)) - [typeRoots or paths](https://github.com/Microsoft/TypeScript/issues/22217#issuecomment-369783776) - [DefinitelyTyped][] --- - [上一章:类型断言](type-assertion.md) - [下一章:内置对象](built-in-objects.md) [DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped/