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