為什么需要聲明?
聲明的本質是告知編譯器一個標識符的類型信息。同時,在使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能。
聲明在TypeScript中至關重要,只有通過聲明才能告知編譯器這個標識符到底代表什么含義。對於語言關鍵字之外的任意標識符,如果編譯器無法獲取它的聲明,將會報錯:
// 錯誤,憑空出現的variable, 編譯器無法知道它代表什么含義
// error TS2304: Cannot find name 'variable'
console.log(variable);
改正這個錯誤,需要添加 variable 的聲明信息:
// 聲明語句
let variable: number;
// 正確,已聲明variable為數字
console.log(variable);
雖然編譯成JavaScript代碼執行時仍然會報錯,但因為添加了variable的聲明信息,在TypeScript中邏輯合理,不會報錯。
內部聲明
到目前為止,所有在TypeScript源碼(ts/tsx結尾的文件,d.ts不算)中出現的聲明,都是內部聲明:
// 聲明a為一個數字
let a: number;
// 聲明b為一個數字並初始化為2
let b: number = 2;
// 聲明T為一個接口
interface T {}
// 聲明接口類型變量b
let b: T;
// 聲明fn為一個函數
function fn(){}
// 聲明myFunc為一個函數
// 此處利用了類型推導
let myFunc = function(a: number){}
// 聲明MyEnum枚舉類型
enum MyEnum {
A, B
}
// 聲明NS為命名空間
namespace NS {}
// ...
內部聲明主要是你當前所寫的代碼中的所有變量和類型的聲明。
外部聲明
外部聲明一般針對第三方來歷不明的庫,當你想要在你的typescript項目中使用用javascript代碼寫的第三方庫時,就需要用到外部聲明。一個常見的例子,假設我們在HTML中通過script標簽引入了全局jQuery:
// 注冊全局變量 $
<script src="path/to/jquery.js"></script>
path/to/jquery.js 文件在會在全局作用域中引入對象 $,接下來如果在同一項目下的TypeScript文件中使用 $,TypeScript編譯器會報錯:
// 錯誤,缺少名字 $ 的聲明信息
// error TS2581: Cannot find name '$'. Do you need to install type definitions for jQuery? Try `npm i @types/jquery`
$('body').html('hello world');
由於沒有任何類型信息,TypeScript編譯器根本不知道 $ 代表的是什么,此時需要引入外部聲明(因為$是外部JavaScript引入TypeScript代碼中的)。外部聲明的關鍵字是:declare
分析語句 $('body').html('hello world'); 得出:
$是一個函數,接收字符串參數$調用返回值是一個對象,此對象擁有成員函數html,這個成員函數的參數也是字符串類型
// 聲明 $ 的類型信息
declare let $: (selector: string) => {
html: (content: string) => void;
};
// 正確,$已經通過外部聲明
$('body').html('hello world');
聲明應該是純粹對於一個標識符類型或外觀的描述,便於編譯器識別,外部聲明具有以下特點:
- 必須使用
declare修飾外部聲明 - 不能包含實現或初始化信息(內部聲明可以在聲明的時候包含實現或初始化)
// 聲明a為一個數字
declare let a: number;
// 錯誤,外部聲明不能初始化
// error TS1039: Initializers are not allowed in ambient contexts
declare let b: number = 2;
// 聲明T為一個接口
declare interface T {}
// 聲明接口類型變量b
let b: T;
// 聲明fn為一個函數
// 錯誤,聲明包含了函數實現
// error TS1183: An implementation cannot be declared in ambient contexts
declare function fn(){}
// 正確,不包含函數體實現
declare function fn(): void;
// 聲明myFunc為一個函數
declare let myFunc: (a: number) => void;
// 聲明MyEnum枚舉類型
declare enum MyEnum {
A, B
}
// 聲明NS為命名空間
declare namespace NS {
// 錯誤,聲明不能初始化
// error TS1039: Initializers are not allowed in ambient contexts
const a: number = 1;
// 正確,僅包含聲明
const b: number;
// 正確,函數未包含函數體實現
function c(): void;
}
// 聲明一個類
declare class Greeter {
constructor(greeting: string);
greeting: string;
showGreeting(): void;
}
外部聲明還可以用於聲明一個模塊,如果一個外部模塊的成員要被外部訪問,模塊成員應該用 export 聲明導出:
declare module 'io' {
export function read(file: string): string;
export function write(file: string, data: string): void;
}
聲明文件
通常我們會把聲明語句放到一個單獨的文件(jQuery.d.ts)中,這就是聲明文件。
聲明文件必需以
.d.ts為后綴
一般來說,ts 會解析項目中所有的 *.ts 文件,當然也包含以 .d.ts 結尾的文件。所以當我們將 jQuery.d.ts 放到項目中時,其他所有 *.ts 文件就都可以獲得 jQuery 的類型定義了。
/path/to/project
├── src
| ├── index.ts
| └── jQuery.d.ts
└── tsconfig.json
假如仍然無法解析,那么可以檢查下 tsconfig.json 中的 files、include 和 exclude 配置,確保其包含了 jQuery.d.ts 文件。
這里只演示了全局變量這種模式的聲明文件,假如是通過模塊導入的方式使用第三方庫的話,那么引入聲明文件又是另一種方式了。
當一個第三方庫沒有提供聲明文件時,我們就需要自己書寫聲明文件了。前面只介紹了最簡單的聲明文件內容,而真正書寫一個聲明文件並不是一件簡單的事,以下會詳細介紹如何書寫聲明文件。
在不同的場景下,聲明文件的內容和使用方式會有所區別。
庫的使用場景主要有以下幾種:
- 全局變量:通過 `` 標簽引入第三方庫,注入全局變量
- npm 包:通過
import foo from 'foo'導入,符合 ES6 模塊規范 - UMD 庫:既可以通過 `` 標簽引入,又可以通過
import導入 - 直接擴展全局變量:通過 `` 標簽引入后,改變一個全局變量的結構
- 在 npm 包或 UMD 庫中擴展全局變量:引用 npm 包或 UMD 庫后,改變一個全局變量的結構
- 模塊插件:通過 `` 或
import導入后,改變另一個模塊的結構
下面主要介紹全局變量的方式:
全局變量是最簡單的一種場景,之前舉的例子就是通過 標簽引入 jQuery,注入全局變量$和jQuery。
使用全局變量的聲明文件時,如果是以 npm install @types/xxx --save-dev 安裝的,則不需要任何配置。如果是將聲明文件直接存放於當前項目中,則建議和其他源碼一起放到 src 目錄下(或者對應的源碼目錄下):
/path/to/project
├── 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 沒有什么區別:
// 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 定義時,表示此時的全局變量是一個常量,不允許再去修改它的值了4:
// 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。
需要注意的是,聲明語句中只能定義類型,切勿在聲明語句中定義具體的實現5:
declare const jQuery = function(selector) {
return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.
declare function
declare function 用來定義全局函數的類型。jQuery 其實就是一個函數,所以也可以用 function 來定義:
// src/jQuery.d.ts
declare function jQuery(selector: string): any;
// src/index.ts
jQuery('#foo');
在函數類型的聲明語句中,函數重載也是支持的6:
// 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 來定義它的類型7:
// src/Animal.d.ts
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
// src/index.ts
let cat = new Animal('Tom');
同樣的,declare class 語句也只能用來定義類型,不能用來定義具體的實現,比如定義 sayHi 方法的具體實現則會報錯:
// 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),舉例如下8:
// 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
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 來聲明這個擁有多個子屬性的全局變量。
// 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 來聲明函數,而不是使用 declare function ajax。類似的,也可以使用 const, class, enum 等語句9:
// 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 來聲明深層的屬性的類型10:
// 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 等其他屬性或方法),則可以不需要嵌套 namespace11:
// 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 來聲明一個全局的接口或類型12:
// 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 類似,不再贅述。
防止命名沖突
暴露在最外層的 interface 或 type 會作為全局類型作用於整個項目中,我們應該盡可能的減少全局變量或全局類型的數量。故最好將他們放到 namespace 下13:
// 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()(事實確實如此),那么我們可以組合多個聲明語句,它們會不沖突的合並起來14:
// 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');
關於聲明合並的更多用法,可以查看聲明合並章節。
總結
聲明的本質是告知編譯器一個標識符的類型信息。相應的聲明文件能讓我們在使用第三方庫時便於獲得對應的代碼補全、接口提示等功能。
以上內容只是部分講解,主要引導不懂d.ts文件的同學知道其用途,想知道如何編寫聲明文件請查詢具體文檔,見參考部分。
