TS學習隨筆(七)->聲明文件

now咱們來看一看TS怎麼聲明文件,javascript

在JS裏面咱們常常會使用各類第三方類庫,引入方式也不太相同,常見的就是在HTML中經過script標籤引入,而後就可使用全局變量$或者jQuery了html

咱們一般這樣獲取一個 id 是 foo 的元素:java

$('#foo'); // or
jQuery('#foo');

可是TS就比較呆滯一點了,在TS中,編譯器並不知道 $ 或 jQuery 是什麼東西:node

jQuery('#foo'); // ERROR: Cannot find name 'jQuery'.

那咱們怎麼解決,咱們可使用declare var來定義類型jquery

declare var jQuery: (selector:string) => any; jQuery('#foo')

上例中,declare var 並無真的定義一個變量,只是定義了全局變量 jQuery 的類型,僅僅會用於編譯時的檢查,在編譯結果中會被刪除。它編譯結果是:git

jQuery('#foo');

除了 declare var 以外,還有其餘不少種聲明語句,咱們會在後面學習。github

什麼是聲明文件

一般咱們會把聲明語句放到一個單獨的文件(jQuery.d.ts)中,這就是聲明文件ajax

// src/jQuery.d.ts
 declare var jQuery: (selector: string) => any;

聲明文件必需以 .d.ts 爲後綴。typescript

通常來講,ts 會解析項目中全部的 *.ts 文件,固然也包含以 .d.ts 結尾的文件。因此當咱們將 jQuery.d.ts 放到項目中時,其餘全部 *.ts 文件就均可以得到 jQuery 的類型定義了。npm

/path/to/project
├── README.md ├── src | ├── index.ts | └── jQuery.d.ts └── tsconfig.json

假如仍然沒法解析,那麼能夠檢查下 tsconfig.json 中的 filesinclude 和 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 中的 filesinclude 和 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。相似的,也可使用 constclassenum 等語句:

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 包的聲明文件可能存在於兩個地方:

  1. 與該 npm 包綁定在一塊兒。判斷依據是 package.json 中有 types 字段,或者有一個 index.d.ts 聲明文件。這種模式不須要額外安裝其餘包,是最爲推薦的,因此之後咱們本身建立 npm 包的時候,最好也將聲明文件與 npm 包綁定在一塊兒。
  2. 發佈到了 @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 字段。

目錄結構:

/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;

注意,只有 functionclass 和 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

相關文章
相關標籤/搜索