TypeScript 為 JavaScriopt 帶來了強(qiáng)類型特性,這就意味著限制了類型的自由度。同一段程序,為了適應(yīng)不同的類型,就可能需要寫不同的處理函數(shù)——而且這些處理函數(shù)中所有邏輯完全相同,唯一不同的就是類型——這嚴(yán)重違反抽象和復(fù)用代碼的原則。
我們來模擬一個場景:某個服務(wù)提供了一些不同類型的數(shù)據(jù),我們需要先通過一個中間件對這些數(shù)據(jù)進(jìn)行一個基本的處理(比如驗證,容錯等),再對其進(jìn)行使用。那么用 JavaScript 來寫應(yīng)該是這樣的
// 模擬服務(wù),提供不同的數(shù)據(jù)。這里模擬了一個字符串和一個數(shù)值
var service = {
getStringValue: function() {
return "a string value";
},
getNumberValue: function() {
return 20;
}
};
// 處理數(shù)據(jù)的中間件。這里用 log 來模擬處理,直接返回數(shù)據(jù)當(dāng)作處理后的數(shù)據(jù)
function middleware(value) {
console.log(value);
return value;
}
// JS 中對于類型并不關(guān)心,所以這里沒什么問題
var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());
先來看看對服務(wù)的改寫,TypeScript 版的服務(wù)有返回類型:
const service = {
getStringValue(): string {
return "a string value";
},
getNumberValue(): number {
return 20;
}
};
為了保證在對 sValue
和 nValue
的后續(xù)操作中類型檢查有效,它們也會有類型(如果 middleware
類型定義得當(dāng),可以推導(dǎo),這里我們先顯示定義其類型)
const sValue: string = middleware(service.getStringValue());
const nValue: number = middleware(service.getNumberValue());
現(xiàn)在的問題是 middleware
要怎么樣定義才既可能返回 string
,又可能返回 number
,而且還能被類型檢查正確推導(dǎo)出來?
any
function middleware(value: any): any {
console.log(value);
return value;
}
是的,這個辦法可以檢查通過。但它的問題在于 middleware
內(nèi)部失去了類型檢查,在后在對 sValue
和 nValue
賦值的時候,也只是當(dāng)作類型沒有問題。簡單的說,是有“假裝”沒問題。
middleware
function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }
當(dāng)然也可以用 TypeScript 的重載(overload)來實現(xiàn)
function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
// 實現(xiàn)一樣沒有嚴(yán)格的類型檢查
}
這種方法最主要的一個問題是……如果我有 10 種類型的數(shù)據(jù),就需要定義 10 個函數(shù)(或重載),那 20 個,200 個呢……
現(xiàn)在我們切入正題,用泛型來解決這個問題。那么這就需要解釋一下什么是泛型了:泛型就是指定一個表示類型的變量,用它來代替某個實際的類型用于編程,而后通過實際調(diào)用時傳入或推導(dǎo)的類型來對其進(jìn)行替換,以達(dá)到一段使用泛型程序可以實際適應(yīng)不同類型的目的。
雖然這個解釋已經(jīng)很接地氣了,但是理解起來還是不如一個實例來得容易。我們來看看 middleware
的泛型實現(xiàn)是怎么樣的
function middleware<T>(value: T): T {
console.log(value);
return value;
}
middleware
后面緊接的 <T>
表示聲明一個表示類型的變量,Value: T
表示聲明參數(shù)是 T
類型的,后面的 : T
表示返回值也是 T
類型的。那么在調(diào)用 middlewre(getStringValue())
的時候,由于參數(shù)推導(dǎo)出來是 string
類型,所以這個時候 T
代表了 string
,因此此時 middleware
的返回類型也就是 string
;而對于 middleware(getNumberValue())
調(diào)用來說,這里的 T
表示了 number
。
我們直接從 VSCode 的提示可以看出來,對于 middleware<T>()
調(diào)用,TypeScript 可以推導(dǎo)出參數(shù)類型和返回值類型:
我們也可以在調(diào)用的時候,小括號前顯示指定 T
代替的類型,比如 mdiddleware<string>(...)
,不過如果指定的類型與推導(dǎo)的類型有沖突,就會提示錯誤:
前面已經(jīng)解釋了“泛型”這個概念。示例中泛型的用法我們稱之為“泛型函數(shù)”。不過泛型更廣泛的用法是用于“泛型類”——即在聲明類的時候聲明泛型,那么在類的整個個作用域范圍內(nèi)都可以使用聲明的泛型類型。
相信大家都已經(jīng)對數(shù)組有所了解,比如 string[]
表示字符串?dāng)?shù)組類型。其實在早期的 TypeScript 版本中沒有這種數(shù)組類型表示,而是采用實例化的泛型 Array<string>
來表示的,現(xiàn)在仍然可以使用這方式來表示數(shù)組。
除此之外,TypeScript 中還有一個很常用的泛型類,Promise<T>
。因為 Promise 往往是帶數(shù)據(jù)的,所以通過 Promise<T>
這種泛型定義的形式,可以表示一個 Promise 所帶數(shù)據(jù)的類型。比如下圖就可以看出,TypeScript 能正確推導(dǎo)出 n
的類型是 number
:
所以,泛型類其實多數(shù)時候是應(yīng)用于容器類。假設(shè)我們需要實現(xiàn)一個 FilteredList
,我們可以向其中 add()
(添加) 任意數(shù)據(jù),但是它在添加的時候會自動過濾掉不符合條件的一些,最終通過 get all()
輸出所有符合條件的數(shù)據(jù)(數(shù)組)。而過濾條件在構(gòu)造對象的時候,以函數(shù)或 Lambda 表達(dá)式提供。
// 聲明泛型類,類型變量為 T
class FilteredList<T> {
// 聲明過濾器是以 T 為參數(shù)類型,返回 boolean 的函數(shù)表達(dá)式
filter: (v: T) => boolean;
// 聲明數(shù)據(jù)是 T 數(shù)組類型
data: T[];
constructor(filter: (v: T) => boolean) {
this.filter = filter;
}
add(value: T) {
if (this.filter(value)) {
this.data.push(value);
}
}
get all(): T[] {
return this.data;
}
}
// 處理 string 類型的 FilteredList
const validStrings = new FilteredList<string>(s => !s);
// 處理 number 類型的 FilteredList
const positiveNumber = new FilteredList<number>(n => n > 0);
甚至還可以把 (v: T) => boolean
聲明為一個類型,以便復(fù)用
type Predicate<T> = (v: T) => boolean;
class FilteredList<T> {
filter: Predicate<T>;
data: T[];
constructor(filter: Predicate<T>) { ... }
add(value: T) { ... }
get all(): T[] { ... }
}
當(dāng)然類型變量也不一定非得叫 T
,也可以叫 TValue
或別的什么,但是一般建議以大寫的 T
作為前綴,采用 Pascal 命名規(guī)則,方便識別。還有一些常見的指代,比如 TKey
表示鍵類型,TValue
表示值類型等(常用于映射表這類容器定義)。
有了泛型之后,一個函數(shù)或容器類能處理的類型一下子擴(kuò)到了無限大,似乎有點失控的感覺。所以這里又產(chǎn)生了一個約束的概念。我們可以聲明對類型參數(shù)進(jìn)行約束。
比如,我們有 IAnimal
這樣一個接口,然后寫一個 run
工具函數(shù),它可以讓動物跑起來,而且它會返回這個動物實例本身(以便鏈?zhǔn)秸{(diào)用)。先來定義類型
interface IAnimal {
run(): void;
}
class Dog implements IAnimal {
run(): void {
console.log("Dog is running");
}
}
function run(animal: IAnimal): IAnimal {
animal.run();
return animal;
}
const dog = run(new Dog()); // dog: IAnimal
這種定義的缺點是 dog 被推導(dǎo)成 IAnimal
類型,當(dāng)然可以通過強(qiáng)制聲明為 const dog: Dog
來指定其類型,但是誰知道 run()
返回的是 Dog
而不是 Cat
呢。
function run<TAnimal>(animal: TAnimal): TAnimal {
animal.run(); // 'run' does not exist on type 'TAnimal'
return animal;
}
采用這種定義,dog 可以推導(dǎo)正確。不過由于 TAnimal
在這里只是個變量,可以代表任意類型,所以它并不能保證擁有 run()
方法可供調(diào)用。
正解是使用泛型約束,將 TAnimal
約束為實現(xiàn)了 IAnimal
。這需要在定義類型變量的使用使用 extends
來約束:
function run<TAnimal extends IAnimal>(animal: TAnimal): TAnimal {
animal.run(); // it's ok
return animal;
}
注意這里的語法,<TAnimal extends IAnimal>
,雖然 IAnimal
是個接口,但這里不是在實現(xiàn)接口,extends
表示約束關(guān)系,而非繼承。它表示 extends
左邊的類型變量實現(xiàn)了右邊的類型,或者是右邊類型的子孫類,或者就是右邊的那個類型。簡單的說,就是左邊類型的實例可以賦值給右邊類型的變量。
有時候我們希望傳入某個工具方法的參數(shù)是一個類型,這樣就可以通過 new
來生成對象。這在 TypeScript 中通常是使用構(gòu)造函數(shù)來約束的,比如
function create<T extends IAnimal>(type: { new(): T }) {
return new type();
}
const dog = create(Dog);
這里約束了 create
可以創(chuàng)建動物的實例。如果不加 extends IAnimal
,那么這個 create
可以創(chuàng)建任何類型的實例。
在使用泛型的時候,當(dāng)然不會限制只使用一個類型變量,我們可以使用多個,比如可以這樣定義一個 Pair
類
class Pair<TKey, TValue> {
private _key: TKey;
private _value: TValue;
constructor(key: TKey, value: TValue) {
this._key = key;
this._value = value;
}
get key() { return this._key; }
get value() { return this._value; }
}
自己定義泛型結(jié)構(gòu)(泛型類或泛型函數(shù))通常只會在寫比較復(fù)雜的應(yīng)用時發(fā)生。但是使用已定義好的泛型是極其常見的,上面已經(jīng)提到了兩個常見的泛型定義,T[]/Array<T>
和 Promise<T>
,除此之外,還有 ES6 的 Set
和 Map
對應(yīng)于 TypeScript 的泛型定義 Set<T>
和 Map<TK, TV>
。另外,泛型還常用于 Generator 和 Iterable/Iterator:
// 產(chǎn)生 n 個隨機(jī)整數(shù)
function* randomInt(n): Iterable<number> {
for (let i = 0; i < n; i++) {
yield ~~(Math.random() * Number.MAX_SAFE_INTEGER);
}
}
for (let n of randomInt(10)) {
console.log(n);
}
敬請 掃碼 關(guān)注〔邊城〕的公眾號:邊城客棧
更多建議: