# 聲明文件
當使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能。
## 新語法索引
由于本章涉及大量新語法,故在本章開頭列出新語法的索引,方便大家在使用這些新語法時能快速查找到對應的講解:
- [`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/