-
declare var
聲明全局變量 -
declare function
聲明全局方法 -
declare class
聲明全局類 -
declare enum
聲明全局枚舉類型 -
declare namespace
聲明(含有子屬性的)全局對象 -
interface
和type
聲明全局類型 -
export
導出變量 -
export namespace
導出(含有子屬性的)對象 -
export default
ES6 默認導出 -
export = commonjs
導出模塊 -
export as namespace
UMD 庫聲明全局變量 -
declare global
擴展全局變量 -
declare module
擴展模塊 -
/// <reference />
三斜線指令
查找需要的聲明文件
書寫聲明文件的幾種場景
- 全局變量: 通過
<script>
標簽引入第三方庫, 注入全局變量 - npm 包: 通過 import foo from 'foo' 導入, 符合 ES6 模塊規(guī)范
- UMD 庫: 既可以通過
<script>
標簽引入, 又可以通過 import 導入 - 直接擴展全局變量: 通過
<script>
標簽引入后, 改變一個全局變量的結構 - 在 npm 包或 UMD 庫中擴展全局變量: 引用 npm 包或 UMD 庫后, 改變一個全局變量的結構
- 模塊插件: 通過
<script>
或 import 導入后, 改變另一個模塊的結構
全局變量
例如通過 <script>
標簽引入 jQuery
, 注入全局變量 $
和 jQuery
.
使用全局變量的聲明文件時, 如果是以 npm install @types/xxx --save-dev
安裝的, 則不需要任何配置.如果是將聲明文件直接存放于當前項目中, 則建議和其他源碼一起放到 src
目錄下(或者對應的源碼目錄下):
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json
全局變量的聲明文件主要有以下幾種語法:
-
declare var
聲明全局變量 -
declare function
聲明全局方法 -
declare class
聲明全局類 -
declare enum
聲明全局枚舉類型 -
declare namespace
聲明(含有子屬性的)全局對象 -
interface
和type
聲明全局類型(注意:不需要declare
)
declare var
在所有的聲明語句中, declare var
是最簡單的, 如之前所學, 它能夠用來定義一個全局變量的類型.與其類似的, 還有 declare let
和 declare const
, 使用 let
與使用 var
沒有什么區(qū)別:
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any
// src/index.ts
jQuery('#foo');
// 使用 declare let 定義的 jQuery 類型, 允許修改這個全局變量
jQuery = function(selector) {
return document.querySelector(selector);
};
一般來說, 全局變量都是禁止修改的常量, 所以大部分情況都應該使用 const
而不是 var
或 let
.
// 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.
需要注意的是, 聲明語句中只能定義類型, 切勿在聲明語句中定義具體的實現(xiàn):
declare const jQuery = function(selector) {
return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.
declare function
declare function
用來定義全局函數(shù)的類型.jQuery
其實就是一個函數(shù), 所以也可以用 function
來定義: (對比和 declare const
寫法的區(qū)別)
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
// src/index.ts
jQuery('#foo');
在函數(shù)類型的聲明語句中, 函數(shù)重載也是支持的:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
// src/index.ts
jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
});
declare class
當全局變量是一個類的時候, 我們用 declare class
來定義它的類型:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
// src/index.ts
let cat = new Animal('Tom');
同樣的, declare class
語句也只能用來定義類型, 不能用來定義具體的實現(xiàn), 比如定義 sayHi
方法的具體實現(xiàn)則會報錯:
// 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
), 舉例如下:
// src/Directions.d.ts
declare enum Directions {
Up,
Down,
Left,
Right
}
// src/index.ts
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
與其他全局變量的類型聲明一致, declare enum
僅用來定義類型, 而不是具體的值.
Directions.d.ts
僅僅會用于編譯時的檢查, 聲明文件里的內容在編譯結果中會被刪除.它編譯結果是:
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
其中 Directions
是由第三方庫定義好的全局變量.
declare namespace
一句話總結:
declare class
聲明一個類,declare namespace
聲明一個對象.
namespace
是 ts
早期時為了解決模塊化而創(chuàng)造的關鍵字, 中文稱為命名空間.
由于歷史遺留原因, 在早期還沒有 ES6 的時候, ts
提供了一種模塊化方案, 使用 module
關鍵字表示內部模塊.但由于后來 ES6
也使用了 module
關鍵字, ts
為了兼容 ES6
, 使用 namespace
替代了自己的 module
, 更名為命名空間.
隨著 ES6
的廣泛應用, 現(xiàn)在已經(jīng)不建議再使用 ts
中的 namespace
, 而推薦使用 ES6
的模塊化方案了, 故我們不再需要學習 namespace
的使用了.
namespace
被淘汰了, 但是在聲明文件中, declare namespace
還是比較常用的, 它用來表示全局變量是一個對象, 包含很多子屬性.
比如 jQuery
是一個全局變量, 它是一個對象, 提供了一個 jQuery.ajax
方法可以調用, 那么我們就應該使用 declare namespace jQuery
來聲明這個擁有多個子屬性的全局變量.
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// src/index.ts
jQuery.ajax('/api/get_something');
注意, 在 declare namespace
內部, 我們直接使用 function ajax
來聲明函數(shù), 而不是使用 declare function ajax
.類似的, 也可以使用 const, class, enum
等語句:
// 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
}
}
// src/index.ts
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
嵌套的命名空間
如果對象擁有深層的層級, 則需要用嵌套的 namespace
來聲明深層的屬性的類型:
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}
// src/index.ts
jQuery.ajax('/api/get_something');
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});
假如 jQuery
下僅有 fn
這一個屬性(沒有 ajax
等其他屬性或方法), 則可以不需要嵌套 namespace
:
// src/jQuery.d.ts
declare namespace jQuery.fn {
function extend(object: any): void;
}
// src/index.ts
jQuery.fn.extend({
check: function() {
return this.each(function() {
this.checked = true;
});
}
});
interface 和 type
除了全局變量之外, 可能有一些類型我們也希望能暴露出來.在類型聲明文件中, 我們可以直接使用 interface
或 type
來聲明一個全局的接口或類型:
// src/jQuery.d.ts
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
這樣的話, 在其他文件中也可以使用這個接口或類型了:
// src/index.ts
let settings: AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);
type
與 interface
類似, 不再贅述.
聲明合并
假如 jQuery
既是一個函數(shù), 可以直接被調用 jQuery('#foo')
, 又是一個對象, 擁有子屬性 jQuery.ajax()
(事實確實如此), 那么我們可以組合多個聲明語句, 它們會不沖突的合并起來:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// src/index.ts
jQuery('#foo');
jQuery.ajax('/api/get_something');
關于聲明合并的更多用法, 可以查看聲明合并章節(jié).
npm
包
一般我們通過 import foo from 'foo'
導入一個 npm
包, 這是符合 ES6
模塊規(guī)范的.
在我們嘗試給一個 npm
包創(chuàng)建聲明文件之前, 需要先看看它的聲明文件是否已經(jīng)存在.一般來說, npm
包的聲明文件可能存在于兩個地方:
- 與該
npm
包綁定在一起.判斷依據(jù)是package.json
中有types
字段, 或者有一個index.d.ts
聲明文件.這種模式不需要額外安裝其他包, 是最為推薦的, 所以以后我們自己創(chuàng)建npm
包的時候, 最好也將聲明文件與npm
包綁定在一起. - 發(fā)布到
@type
s 里.我們只需要嘗試安裝一下對應的@types
包就知道是否存在該聲明文件, 安裝命令是npm install @types/foo --save-dev
.這種模式一般是由于npm
包的維護者沒有提供聲明文件, 所以只能由其他人將聲明文件發(fā)布到@types
里了.
假如以上兩種方式都沒有找到對應的聲明文件, 那么我們就需要自己為它寫聲明文件了.由于是通過 import
語句導入的模塊, 所以聲明文件存放的位置也有所約束, 一般有兩種方案:
- 創(chuàng)建一個
node_modules/@types/foo/index.d.ts
文件, 存放foo
模塊的聲明文件.這種方式不需要額外的配置, 但是node_modules
目錄不穩(wěn)定, 代碼也沒有被保存到倉庫中, 無法回溯版本, 有不小心被刪除的風險, 故不太建議用這種方案, 一般只用作臨時測試. - 創(chuàng)建一個
types
目錄, 專門用來管理自己寫的聲明文件, 將foo
的聲明文件放到types/foo/index.d.ts
中.這種方式需要配置下tsconfig.json
中的paths
和baseUrl
字段.
目錄結構:
/path/to/project
├── src
| └── index.ts
├── types
| └── foo
| └── index.d.ts
└── tsconfig.json
tsconfig.json
內容(關于 baseUrl
與 paths
的作用,我在其他教程中有總結):
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
如此配置之后, 通過 import
導入 foo
的時候, 也會去 types
目錄下尋找對應的模塊的聲明文件了.
注意 module
配置可以有很多種選項, 不同的選項會影響模塊的導入導出模式.這里我們使用了 commonjs
這個最常用的選項, 后面的教程也都默認使用的這個選項.
不管采用了以上兩種方式中的哪一種, 我都強烈建議大家將書寫好的聲明文件(通過給第三方庫發(fā) pull request
, 或者直接提交到 @types
里)發(fā)布到開源社區(qū)中, 享受了這么多社區(qū)的優(yōu)秀的資源, 就應該在力所能及的時候給出一些回饋.只有所有人都參與進來, 才能讓 ts
社區(qū)更加繁榮.
npm
包的聲明文件主要有以下幾種語法:
-
export const/fucntion/class/enum/interface
導出變量 -
export namespace
導出(含有子屬性的)對象 -
export default
ES6 默認導出 -
export = commonjs
導出模塊
export
npm
包的聲明文件與全局變量的聲明文件有很大區(qū)別.在 npm
包的聲明文件中, 使用 declare
不再會聲明一個全局變量(當前文件中含有 import/export
關鍵字,系統(tǒng)會自動識別為模塊), 而只會在當前文件中聲明一個局部變量.只有在聲明文件中使用 export
導出, 然后在使用方 import
導入后, 才會應用到這些類型聲明.
export
的語法與普通的 ts
中的語法類似, 區(qū)別僅在于聲明文件中禁止定義具體的實現(xiàn):
// 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;
}
對應的導入和使用模塊應該是這樣:
// 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'
}
};
同樣, 上文中的 Directions
是由庫提供真正的內容.
混用 declare
和 export
我們也可以使用 declare
先聲明多個變量, 最后再用 export
一次性導出.上例的聲明文件可以等價的改寫為:
// 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
用來導出一個擁有子屬性的對象:
// types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
// src/index.ts
import { foo } from 'foo';
console.log(foo.name);
foo.bar.baz();
export default
在 ES6
模塊系統(tǒng)中, 使用 export default
可以導出一個默認值, 使用方可以用 import foo from 'foo'
而不是 import { foo } from 'foo'
來導入這個默認值.
在類型聲明文件中, export default
用來導出默認值的類型:
// types/foo/index.d.ts
export default function foo(): string;
// src/index.ts
import foo from 'foo';
foo();
注意, 只有 function
究西、class
和 interface
可以直接默認導出, 其他的變量需要先定義出來, 再默認導出:
// types/foo/index.d.ts
export default enum Directions {
// ERROR: Expression expected.
Up,
Down,
Left,
Right
}
上例中 export default enum
是錯誤的語法, 需要使用 declare enum
定義出來, 然后使用 export default
導出:
// types/foo/index.d.ts
declare enum Directions {
Up,
Down,
Left,
Right
}
export default Directions;
針對這種默認導出, 我們一般會將導出語句放在整個聲明文件的最前面:
// types/foo/index.d.ts
export default Directions;
declare enum Directions {
Up,
Down,
Left,
Right
}
export =
在 commonjs
規(guī)范中, 我們用以下方式來導出一個模塊:
// 整體導出
module.exports = foo;
// 單個導出
exports.bar = bar;
在 ts
中, 針對這種模塊導出, 有多種方式可以導入:
第一種方式是 const ... = require
:
// 整體導入
const foo = require('foo');
// 單個導入
const bar = require('foo').bar;
第二種方式是 import ... from
, 注意針對整體導出, 需要使用 import * as
來導入:
// 整體導入
import * as foo from 'foo';
// 單個導入
import { bar } from 'foo';
第三種方式是 import ... require
, 這也是 ts
官方推薦的方式:
// 整體導入
import foo = require('foo');
// 單個導入
import bar = foo.bar;
對于這種使用 commonjs
規(guī)范的庫, 假如要為它寫類型聲明文件的話, 就需要使用到 export =
這種語法了(來表明當前引用的庫是 commonjs
風格的):
// 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
規(guī)范和 commonjs
規(guī)范而創(chuàng)立的新語法, 由于并不常用也不推薦使用, 所以這里就不詳細介紹了, 感興趣的可以看官方文檔.
由于很多第三方庫是 commonjs
規(guī)范的, 所以聲明文件也就不得不用到 export =
這種語法了.但是還是需要再強調下, 相比與 export =
, 我們更推薦使用 ES6
標準的 export default
和 export
.
UMD
庫
既可以通過 <script>
標簽引入, 又可以通過 import
導入的庫, 稱為 UMD
庫.相比于 npm
包的類型聲明文件, 我們需要額外聲明一個全局變量, 為了實現(xiàn)這種方式, ts
提供了一個新語法 export as namespace
.
export as namespace
一般使用 export as namespace
時, 都是先有了 npm
包的聲明文件, 再基于它添加一條 export as namespace
語句, 即可將聲明好的一個變量聲明為全局變量, 舉例如下:
// types/foo/index.d.ts
export as namespace foo;
export = foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
當然它也可以與 export default
一起使用:
// types/foo/index.d.ts
export as namespace foo;
export default foo;
declare function foo(): string;
declare namespace foo {
const bar: number;
}
直接擴展全局變量
直接使用
interface
或者declare namespace
有的第三方庫擴展了一個全局變量, 可是此全局變量的類型卻沒有相應的更新過來, 就會導致 ts
編譯錯誤, 此時就需要擴展全局變量的類型.比如擴展 String
類型:
interface String {
prependHello(): string;
}
'foo'.prependHello();
通過聲明合并, 使用 interface String
即可給 String
添加屬性或方法.
也可以使用 declare namespace
給已有的命名空間添加類型聲明:
// types/jquery-plugin/index.d.ts
declare namespace JQuery {
interface CustomOptions {
bar: string;
}
}
interface JQueryStatic {
foo(options: JQuery.CustomOptions): string;
}
// src/index.ts
jQuery.foo({
bar: ''
});
在 npm
包或 UMD
庫中擴展全局變量
使用
declare global
如之前所說, 對于一個 npm
包或者 UMD
庫的聲明文件, 只有 export
導出的類型聲明才能被導入.所以對于 npm
包或 UMD
庫, 如果導入此庫之后會擴展全局變量, 則需要使用另一種語法在聲明文件中擴展全局變量的類型, 那就是 declare global
.
declare global
使用 declare global
可以在 npm
包或者 UMD
庫的聲明文件中擴展全局變量的類型:
// types/foo/index.d.ts
declare global {
interface String {
prependHello(): string;
}
}
export {};
// src/index.ts
'bar'.prependHello();
注意:即使此聲明文件不需要導出任何東西, 仍然需要導出一個空對象(即使用export
關鍵字), 用來告訴編譯器這是一個模塊的聲明文件, 而不是一個全局變量的聲明文件.
模塊插件
有時通過 import
導入一個模塊插件, 可以改變另一個原有模塊的結構.此時如果原有模塊已經(jīng)有了類型聲明文件, 而插件模塊沒有類型聲明文件, 就會導致類型不完整, 缺少插件部分的類型.ts
提供了一個語法 declare module
, 它可以用來擴展原有模塊的類型.
declare module
對已經(jīng)存在的模塊擴展時
如果是需要擴展原有模塊的話, 需要在類型聲明文件中先引用原有模塊, 再使用 declare module
擴展原有模塊:
// types/moment-plugin/index.d.ts
import * as moment from 'moment'; // 需先引入原來的模塊
declare module 'moment' { // 擴展原模塊的功能的聲明項
// 寫法同模塊聲明文件
export function foo(): moment.CalendarKey;
}
// src/index.ts
import * as moment from 'moment';
import 'moment-plugin';
moment.foo();
declare module
也可用于在一個文件中一次性聲明多個模塊的類型:
// types/foo-bar.d.ts
// 這其實也是在"全局"下的
declare module 'foo' {
export interface Foo {
foo: string;
}
}
declare module 'bar' {
export function bar(): string;
}
// src/index.ts
import { Foo } from 'foo';
import * as bar from 'bar';
let f: Foo;
bar.bar();
聲明文件中的依賴
一個聲明文件有時會依賴另一個聲明文件中的類型, 比如在前面的 declare module
的例子中, 我們就在聲明文件中導入了 moment
, 并且使用了 moment.CalendarKey
這個類型:
// types/moment-plugin/index.d.ts
import * as moment from 'moment';
declare module 'moment' {
export function foo(): moment.CalendarKey;
}
除了可以在聲明文件中通過 import
導入另一個聲明文件中的類型之外, 還有一個語法也可以用來導入另一個聲明文件, 那就是三斜線指令.
三斜線指令
與 namespace
類似, 三斜線指令也是 ts
在早期版本中為了描述模塊之間的依賴關系而創(chuàng)造的語法.隨著 ES6
的廣泛應用, 現(xiàn)在已經(jīng)不建議再使用 ts
中的三斜線指令來聲明模塊之間的依賴關系了.
但是在聲明文件中, 它還是有一定的用武之地.
類似于聲明文件中的 import
, 它可以用來導入另一個聲明文件.與 import
的區(qū)別是, 當且僅當在以下幾個場景下, 我們才需要使用三斜線指令替代 import
:
- 當我們在書寫一個全局變量的聲明文件時
- 當我們需要依賴一個全局變量的聲明文件時
書寫一個全局變量的聲明文件
這些場景聽上去很拗口, 但實際上很好理解——在全局變量的聲明文件中, 是不允許出現(xiàn) import
, export
關鍵字的.一旦出現(xiàn)了, 那么他就會被視為一個 npm
包或 UMD
庫, 就不再是全局變量的聲明文件了.故當我們在書寫一個全局變量的聲明文件時, 如果需要引用另一個庫的類型, 那么就必須用三斜線指令了:
// types/jquery-plugin/index.d.ts
/// <reference types="jquery" />
declare function foo(options: JQuery.AjaxSettings): string;
// src/index.ts
foo({});
三斜線指令的語法如上, ///
后面使用 xml
的格式添加了對 jquery
類型的依賴, 這樣就可以在聲明文件中使用 JQuery.AjaxSettings
類型了.
注意, 三斜線指令必須放在文件的最頂端, 三斜線指令的前面只允許出現(xiàn)單行或多行注釋.
依賴一個全局變量的聲明文件
在另一個場景下, 當我們需要依賴一個全局變量的聲明文件時, 由于全局變量不支持通過 import
導入, 當然也就必須使用三斜線指令來引入了:
// types/node-plugin/index.d.ts
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;
// src/index.ts
import { foo } from 'node-plugin';
foo(global.process);
在上面的例子中, 我們通過三斜線指引入了 node
的類型, 然后在聲明文件中使用了 NodeJS.Process
這個類型.最后在使用到 foo
的時候, 傳入了 node
中的全局變量 process
.
由于引入的 node
中的類型都是全局變量的類型, 它們是沒有辦法通過 import
來導入的, 所以這種場景下也只能通過三斜線指令來引入了.
以上兩種使用場景下, 都是由于需要書寫或需要依賴全局變量的聲明文件, 所以必須使用三斜線指令.在其他的一些不是必要使用三斜線指令的情況下, 就都需要使用 import 來導入.
拆分聲明文件
當我們的全局變量的聲明文件太大時, 可以通過拆分為多個文件, 然后在一個入口文件中將它們一一引入, 來提高代碼的可維護性.比如 jQuery
的聲明文件就是這樣的:
// 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
兩種不同的指令.它們的區(qū)別是: types
用于聲明對另一個庫的依賴, 而 path
用于聲明對另一個文件的依賴.
上例中, sizzle
是與 jquery
平行的另一個庫, 所以需要使用 types="sizzle"
來聲明對它的依賴.而其他的三斜線指令就是將 jquery
的聲明拆分到不同的文件中了, 然后在這個入口文件中使用 path="foo"
將它們一一引入.
其他三斜線指令
除了這兩種三斜線指令之外, 還有其他的三斜線指令, 比如 /// <reference no-default-lib="true"/>
, /// <amd-module />
等, 但它們都是廢棄的語法, 故這里就不介紹了, 詳情可見官網(wǎng).
自動生成聲明文件§
如果庫的源碼本身就是由 ts
寫的, 那么在使用 tsc
腳本將 ts
編譯為 js
的時候, 添加 declaration
選項, 就可以同時也生成 .d.ts
聲明文件了.
我們可以在命令行中添加 --declaration
(簡寫 -d
), 或者在 tsconfig.json
中添加 declaration
選項.這里以 tsconfig.json
為例:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}
上例中我們添加了 outDir
選項, 將 ts
文件的編譯結果輸出到 lib
目錄下, 然后添加了 declaration
選項, 設置為 true
, 表示將會由 ts
文件自動生成 .d.ts
聲明文件, 也會輸出到 lib
目錄下.
運行 tsc
之后, 目錄結構如下:
/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
.它們的內容分別是:
// src/index.ts
export * from './bar';
export default function foo() {
return 'foo';
}
// src/bar/index.ts
export function bar() {
return 'bar';
}
// lib/index.d.ts
export * from './bar';
export default function foo(): string;
// lib/bar/index.d.ts
export declare function bar(): string;
可見, 自動生成的聲明文件基本保持了源碼的結構, 而將具體實現(xiàn)去掉了, 生成了對應的類型聲明.
使用 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
文件
發(fā)布聲明文件
當我們?yōu)橐粋€庫寫好了聲明文件之后, 下一步就是將它發(fā)布出去了.
此時有兩種方案:
- 將聲明文件和源碼放在一起
- 將聲明文件發(fā)布到
@types
下
這兩種方案中優(yōu)先選擇第一種方案.保持聲明文件與源碼在一起, 使用時就不需要額外增加單獨的聲明文件庫的依賴了, 而且也能保證聲明文件的版本與源碼的版本保持一致.
僅當我們在給別人的倉庫添加類型聲明文件, 但原作者不愿意合并 pull request
時, 才需要使用第二種方案, 將聲明文件發(fā)布到 @types
下.
將聲明文件和源碼放在一起
如果聲明文件是通過 tsc
自動生成的, 那么無需做任何其他配置, 只需要把編譯好的文件也發(fā)布到 npm
上, 使用方就可以獲取到類型提示了.
如果是手動寫的聲明文件, 那么需要滿足以下條件之一, 才能被正確的識別:
- 給
package.json
中的types
或typings
字段指定一個類型聲明文件地址 - 在項目根目錄下, 編寫一個
index.d.ts
文件 - 針對入口文件(
package.json
中的main
字段指定的入口文件), 編寫一個同名不同后綴的.d.ts
文件
第一種方式是給 package.json
中的 types
或 typings
字段指定一個類型聲明文件地址.比如:
{
"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
是這樣時:
{
"name": "foo",
"version": "1.0.0",
"main": "lib/index.js"
}
就會先識別 package.json
中是否存在 types
或 typings
字段.發(fā)現(xiàn)不存在, 那么就會尋找是否存在 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
, 這與自動生成聲明文件類似, 一個庫中同時包含了多個類型聲明文件.
將聲明文件發(fā)布到 @types
下
如果我們是在給別人的倉庫添加類型聲明文件, 但原作者不愿意合并 pull request
, 那么就需要將聲明文件發(fā)布到 @types
下.
與普通的 npm
模塊不同, @types
是統(tǒng)一由 DefinitelyTyped 管理的.要將聲明文件發(fā)布到 @types
下, 就需要給 DefinitelyTyped
創(chuàng)建一個 pull-request
, 其中包含了類型聲明文件, 測試代碼, 以及 tsconfig.json
等.
pull-request
需要符合它們的規(guī)范, 并且通過測試, 才能被合并, 稍后就會被自動發(fā)布到 @types
下.
在 DefinitelyTyped
中創(chuàng)建一個新的類型聲明, 需要用到一些工具, DefinitelyTyped
的文檔中已經(jīng)有了詳細的介紹, 這里就不贅述了, 以官方文檔為準.
如果大家有此類需求, 可以參考下筆者提交的 pull-request.