now我們來看一看TS怎么聲明文件,
在JS里面我們經常會使用各種第三方類庫,引入方式也不太相同,常見的就是在HTML中通過script標簽引入,然后就可以使用全局變量$或者jQuery了
我們通常這樣獲取一個 id
是 foo
的元素:
$('#foo'); // or jQuery('#foo');
但是TS就比較呆滯一點了,在TS中,編譯器並不知道 $
或 jQuery
是什么東西:
jQuery('#foo'); // ERROR: Cannot find name 'jQuery'.
那我們怎么解決,我們可以使用declare var來定義類型
declare var jQuery: (selector:string) => any; jQuery('#foo')
上例中,declare var
並沒有真的定義一個變量,只是定義了全局變量 jQuery
的類型,僅僅會用於編譯時的檢查,在編譯結果中會被刪除。它編譯結果是:
jQuery('#foo');
除了 declare var
之外,還有其他很多種聲明語句,我們會在后面學習。
什么是聲明文件
通常我們會把聲明語句放到一個單獨的文件(jQuery.d.ts
)中,這就是聲明文件
// src/jQuery.d.ts declare var jQuery: (selector: string) => any;
聲明文件必需以 .d.ts
為后綴。
一般來說,ts 會解析項目中所有的 *.ts
文件,當然也包含以 .d.ts
結尾的文件。所以當我們將 jQuery.d.ts
放到項目中時,其他所有 *.ts
文件就都可以獲得 jQuery
的類型定義了。
/path/to/project ├── README.md ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json
假如仍然無法解析,那么可以檢查下 tsconfig.json
中的 files
、include
和 exclude
配置,確保其包含了 jQuery.d.ts
文件。
第三方聲明文件
當然,jQuery 的聲明文件不需要我們定義了,社區已經幫我們定義好了:jQuery in DefinitelyTyped。
我們可以直接下載下來使用,但是更推薦的是使用 @types
統一管理第三方庫的聲明文件
@types
的使用方式很簡單,直接用 npm 安裝對應的聲明模塊即可,以 jQuery 舉例:
npm install @types/jquery --save-dev
書寫聲明文件
當一個第三方庫沒有提供聲明文件時,我們就需要自己書寫聲明文件了。前面只介紹了最簡單的聲明文件內容,而真正書寫一個聲明文件並不是一件簡單的事。以下會詳細介紹如何書寫聲明文件。
在不同的場景下,聲明文件的內容和使用方式會有所區別。
庫的使用場景主要有以下幾種:
- 全局變量:通過
<script>
標簽引入第三方庫,注入全局變量 - npm 包:通過
import foo from 'foo'
導入,符合 ES6 模塊規范 - UMD 庫:既可以通過
<script>
標簽引入,又可以通過import
導入 - 模塊插件:通過
import
導入后,可以改變另一個模塊的結構 - 直接擴展全局變量:通過
<script>
標簽引入后,改變一個全局變量的結構。比如為String.prototype
新增了一個方法 - 通過導入擴展全局變量:通過
import
導入后,可以改變一個全局變量的結構
全局變量
全局變量是最簡單的一種場景,之前舉的例子就是通過 <script>
標簽引入 jQuery,注入全局變量 $
和 jQuery
。
使用全局變量的聲明文件時,如果是以 npm install @types/xxx --save-dev
安裝的,則不需要任何配置。如果是將聲明文件直接存放於當前項目中,則建議和其他源碼一起放到 src
目錄下(或者對應的源碼目錄下):
/path/to/project ├── README.md ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json
如果沒有生效,可以檢查下 tsconfig.json
中的 files
、include
和 exclude
配置,確保其包含了 jQuery.d.ts
文件。
全局變量的聲明文件主要有以下幾種語法:
declare var
聲明全局變量declare function
聲明全局方法declare class
聲明全局類declare enum
聲明全局枚舉類型declare namespace
聲明全局對象(含有子屬性)interface
和type
聲明全局類型
declare var
在所有的聲明語句中,declare var
是最簡單的,如之前所學,它能夠用來定義一個全局變量的類型。與其類似的,還有 declare let
和 declare const
,使用 let
與使用 var
沒有什么區別,而使用 const
定義時,表示此時的全局變量是一個常量,不允許再去修改它的值了:
declare let jQuery: (selector: string) => any; jQuery('#foo'); // 使用 declare let 定義的 jQuery 類型,允許修改這個全局變量 jQuery = function(selector) { return document.querySelector(selector); }
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
。
declare const jQuery = function(selector) { return document.querySelector(selector) }; // ERROR: An implementation cannot be declared in ambient contexts.
declare function
declare function
用來定義全局函數的類型。jQuery 其實就是一個函數,所以也可以用 function
來定義:
declare function jQuery(selector: string): any; jQuery('#foo');
在函數類型的聲明語句中,函數重載也是支持的:
declare function jQuery(selector: string): any; declare function jQuery(domReadyCallback: () => any): any; jQuery('#foo'); jQuery(function() { alert('Dom Ready!'); });
declare class
當全局變量是一個類的時候,我們用 declare class
來定義它的類型:
declare class Animal { constructor(name: string); sayHi(): string; } let cat = new Animal('Tom');
同樣的,declare class
語句也只能用來定義類型,不能用來定義具體的值,比如定義 sayHi
方法的具體實現則會報錯:
declare class Animal { 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),舉例如下:
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
與其他全局變量的類型聲明一致,declare enum
僅用來定義類型,而不是具體的值。它僅僅會用於編譯時的檢查,在編譯結果中會被刪除。它編譯結果是:
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
來聲明這個擁有多個子屬性的全局變量。
declare namespace jQuery { function ajax(url: string, settings?: any): void; } jQuery.ajax('/api/get_something');
注意,在 declare namespace
內部,我們直接使用 function ajax
來聲明函數,而不是使用 declare function ajax
。類似的,也可以使用 const
、class
、enum
等語句:
declare namespace jQuery { function ajax(url: string, settings?: any): void; const version: number; class Event { blur(eventType: EventType): void } enum EventType { CustomClick } } jQuery.ajax('/api/get_something'); console.log(jQuery.version); const e = new jQuery.Event(); e.blur(jQuery.EventType.CustomClick);
在編譯之后,declare namespace
內的所有內容都會被刪除。它的編譯結果是:
jQuery.ajax('/api/get_something'); console.log(jQuery.version); var e = new jQuery.Event(); e.blur(jQuery.EventType.CustomClick);
嵌套的命名空間
如果對象擁有深層的層級,則需要用嵌套的 namespace
來聲明深層的屬性的類型:
declare namespace jQuery { function ajax(url: string, settings?: any): void; namespace fn { function extend(object: any): void; } } jQuery.ajax('/api/get_something'); jQuery.fn.extend({ check: function() { return this.each(function() { this.checked = true; }); } });
假如 jQuery
下僅有 fn
這一個屬性(沒有 ajax
等其他屬性或方法),則可以不需要嵌套 namespace
:
declare namespace jQuery.fn { function extend(object: any): void; } jQuery.fn.extend({ check: function() { return this.each(function() { this.checked = true; }); } });
type和interface
除了全局變量之外,有一些類型我們可能也希望能暴露出來。在類型聲明文件中,我們可以直接使用 interface
或 type
來聲明一個全局的類型:
// src/jQuery.d.ts interface AjaxSettings { method?: 'GET' | 'POST' data?: any; } declare namespace jQuery { function ajax(url: string, settings?: AjaxSettings): void; }
這樣的話,在其他文件中也可以使用這個接口了:
let settings: AjaxSettings = { method: 'POST', data: { name: 'foo' } }; jQuery.ajax('/api/post_something', settings);
type
與 interface
類似,不再贅述。
防止命名沖突
暴露在最外層的 interface
或 type
會作為全局類型作用於整個項目中,我們應該盡可能的減少全局變量或全局類型的數量。故應該將他們放到 namespace
下:
// src/jQuery.d.ts declare namespace jQuery { interface AjaxSettings { method?: 'GET' | 'POST' data?: any; } function ajax(url: string, settings?: AjaxSettings): void; }
注意,在使用這個 interface
的時候,也應該加上 jQuery
前綴了:
// src/index.ts let settings: jQuery.AjaxSettings = { method: 'POST', data: { name: 'foo' } }; jQuery.ajax('/api/post_something', settings);
聲明合並
假如 jQuery 既是一個函數,可以直接被調用 jQuery('#foo')
,又是一個對象,擁有子屬性 jQuery.ajax()
(事實確實如此),則我們可以組合多個聲明語句,它們會不沖突的合並起來:
declare function jQuery(selector: string): any; declare namespace jQuery { function ajax(url: string, settings?: any): void; } jQuery('#foo'); jQuery.ajax('/api/get_something');
npm 包
一般我們通過 import foo from 'foo'
導入一個 npm 包,這是符合 ES6 模塊規范的。
在我們嘗試給一個 npm 包創建聲明文件之前,首先看看它的聲明文件是否已經存在。一般來說,npm 包的聲明文件可能存在於兩個地方:
- 與該 npm 包綁定在一起。判斷依據是
package.json
中有types
字段,或者有一個index.d.ts
聲明文件。這種模式不需要額外安裝其他包,是最為推薦的,所以以后我們自己創建 npm 包的時候,最好也將聲明文件與 npm 包綁定在一起。 - 發布到了
@types
里。只要嘗試安裝一下對應的包就知道是否存在,安裝命令是npm install @types/foo --save-dev
。這種模式一般是由於 npm 包的維護者沒有提供聲明文件,所以只能由其他人將聲明文件發布到@types
里了。
假如以上兩種方式都沒有找到對應的聲明文件,那么我們就需要自己為它寫聲明文件了。由於是通過 import
語句導入的模塊,所以聲明文件存放的位置也有所約束,一般有兩種方案:
- 創建一個
node_modules/@types/foo/index.d.ts
文件,存放foo
模塊的聲明文件。這種方式不需要額外的配置,但是node_modules
目錄不穩定,代碼也沒有被保存到倉庫中,無法回溯版本,有不小心被刪除的風險。 - 創建一個
types
目錄,專門用來管理自己寫的聲明文件,將foo
的聲明文件放到types/foo/index.d.ts
中。這種方式需要配置下tsconfig.json
的paths
和baseUrl
字段。
目錄結構:
/path/to/project ├── README.md ├── src | └── index.ts ├── types | └── foo | └── index.d.ts └── tsconfig.json
tsconfig.json
內容:
{ "compilerOptions": { "module": "commonjs", "baseUrl": "./", "paths": { "*" : ["types/*"] } } }
如此配置之后,通過 import
導入 foo
的時候,也會去 types
目錄下尋找對應的模塊的聲明文件了。
注意 module
配置可以有很多種選項,不同的選項會影響模塊的導入導出模式。這里我們使用了 commonjs
這個最常用的選項,后面的教程也都默認使用的這個選項。
export
npm 包的聲明文件與全局變量的聲明文件有很大區別。在 npm 包的聲明文件中,使用 declare
不再會聲明一個全局變量,而只會在當前文件中聲明一個局部變量。只有在聲明文件中使用 export
導出,然后在使用方 import
導入后,才會應用到這些類型聲明。
export
的語法與非聲明文件中的語法類似,區別僅在於聲明文件中禁止定義具體的值:
// 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' } }
混用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
// 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 模塊系統中,使用 export default
可以導出一個默認值,使用方可以用 import foo from 'foo'
而不是 import { foo } from 'foo'
來導入這個默認值。
在類型聲明文件中,export default
用來導出默認值的類型:
// types/foo/index.d.ts export default function foo(): string;
注意,只有 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 export default Directions; declare enum Directions { Up, Down, Left, Right }
如上例,針對這種默認導出,我們一般會將導出語句放在整個聲明文件的最前面。
export =
在 commonjs 規范中,我們用以下方式來導出:
// 整體導出 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 = require('foo').bar;
對於這種使用 commonjs 規范的庫,假如要給它寫類型聲明文件的話,就需要使用到 export =
這種語法了:
// 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 規范而創立的新語法,由於並不常用也不推薦使用,所以這里就不詳細介紹了,感興趣的可以看官方文檔。
由於很多第三方庫是 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
語句,即可將聲明好的一個變量聲明為全局變量,舉例如下:
// 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; }
直接擴展全局變量
有的時候,我們在代碼里面擴展了一個全局變量,可是它的類型卻沒有相應的更新過來,就會導致 ts 編譯錯誤,此時就需要來擴展全局變量的類型。比如擴展 String
:
interface String {
prependHello(): string;
}
'foo'.prependHello();
通過聲明合並,使用 interface String
即可給全局變量 String
添加屬性或方法
通過導入擴展全局變量
如之前所說,對於一個 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 default function foo(): string;
當使用方導入 foo
之后,就可以使用字符串上的 prependHello
方法了:
// src/index.ts import foo from 'foo'; 'bar'.prependHello();
原文鏈接:https://github.com/xcatliu/typescript-tutorial/blob/master/basics/declaration-files.md