TypeScript之Generics泛型類型學(xué)習(xí)
引言
TypeScript 的官方文檔早已更新,但我能找到的中文文檔都還停留在比較老的版本。所以對(duì)其中新增以及修訂較多的一些章節(jié)進(jìn)行了翻譯整理。
本篇整理自 TypeScript Handbook 中 「Generics」 章節(jié)。
本文并不嚴(yán)格按照原文翻譯,對(duì)部分內(nèi)容也做了解釋補(bǔ)充。
Generics 初探(Hello World of Generics)
軟件工程的一個(gè)重要部分就是構(gòu)建組件,組件不僅需要有定義良好和一致的 API,也需要是可復(fù)用的(reusable)。好的組件不僅能夠兼容今天的數(shù)據(jù)類型,也能適用于未來可能出現(xiàn)的數(shù)據(jù)類型,這在構(gòu)建大型軟件系統(tǒng)時(shí)會(huì)給你最大的靈活度。
在比如 C# 和 Java 語(yǔ)言中,用來創(chuàng)建可復(fù)用組件的工具,我們稱之為泛型(generics)。利用泛型,我們可以創(chuàng)建一個(gè)支持眾多類型的組件,這讓用戶可以使用自己的類型消費(fèi)(consume)這些組件。
讓我們開始寫第一個(gè)泛型,一個(gè)恒等函數(shù)(identity function)。所謂恒等函數(shù),就是一個(gè)返回任何傳進(jìn)內(nèi)容的函數(shù)。你也可以把它理解為類似于 echo
命令。
不借助泛型,我們也許需要給予恒等函數(shù)一個(gè)具體的類型:
function identity(arg: number): number { return arg; }
或者,我們使用 any
類型:
function identity(arg: any): any { return arg; }
盡管使用 any
類型可以讓我們接受任何類型的 arg
參數(shù),但也讓我們丟失了函數(shù)返回時(shí)的類型信息。如果我們傳入一個(gè)數(shù)字,我們唯一知道的信息是函數(shù)可以返回任何類型的值。
所以我們需要一種可以捕獲參數(shù)類型的方式,然后再用它表示返回值的類型。這里我們用了一個(gè)類型變量(type variable),一種用在類型而非值上的特殊的變量。
function identity<Type>(arg: Type): Type { return arg; }
現(xiàn)在我們已經(jīng)給恒等函數(shù)加上了一個(gè)類型變量 Type
,這個(gè) Type
允許我們捕獲用戶提供的類型,使得我們?cè)诮酉聛砜梢允褂眠@個(gè)類型。這里,我們?cè)俅斡?nbsp;Type
作為返回的值的類型。在現(xiàn)在的寫法里,我們可以清楚的知道參數(shù)和返回值的類型是同一個(gè)。
現(xiàn)在這個(gè)版本的恒等函數(shù)就是一個(gè)泛型,它可以支持傳入多種類型。不同于使用 any
,它沒有丟失任何信息,就跟第一個(gè)使用 number
作為參數(shù)和返回值類型的的恒等函數(shù)一樣準(zhǔn)確。
在我們寫了一個(gè)泛型恒等函數(shù)后,我們有兩種方式可以調(diào)用它。第一種方式是傳入所有的參數(shù),包括類型參數(shù):
let output = identity<string>("myString"); // let output: string
在這里,我們使用 <>
而不是 ()
包裹了參數(shù),并明確的設(shè)置 Type
為 string
作為函數(shù)調(diào)用的一個(gè)參數(shù)。
第二種方式可能更常見一些,這里我們使用了類型參數(shù)推斷(type argument inference)(部分中文文檔會(huì)翻譯為“類型推論”),我們希望編譯器能基于我們傳入的參數(shù)自動(dòng)推斷和設(shè)置 Type
的值。
let output = identity("myString"); // let output: string
注意這次我們并沒有用 <>
明確的傳入類型,當(dāng)編譯器看到 myString
這個(gè)值,就會(huì)自動(dòng)設(shè)置 Type
為它的類型(即 string
)。
類型參數(shù)推斷是一個(gè)很有用的工具,它可以讓我們的代碼更短更易閱讀。而在一些更加復(fù)雜的例子中,當(dāng)編譯器推斷類型失敗,你才需要像上一個(gè)例子中那樣,明確的傳入?yún)?shù)。
使用泛型類型變量(Working with Generic Type Variables)
當(dāng)你創(chuàng)建類似于 identity
這樣的泛型函數(shù)時(shí),你會(huì)發(fā)現(xiàn),編譯器會(huì)強(qiáng)制你在函數(shù)體內(nèi),正確的使用這些類型參數(shù)。這就意味著,你必須認(rèn)真的對(duì)待這些參數(shù),考慮到他們可能是任何一個(gè),甚至是所有的類型(比如用了聯(lián)合類型)。
讓我們以 identity
函數(shù)為例:
function identity<Type>(arg: Type): Type { return arg; }
如果我們想打印 arg
參數(shù)的長(zhǎng)度呢?我們也許會(huì)嘗試這樣寫:
function loggingIdentity<Type>(arg: Type): Type { console.log(arg.length); // Property 'length' does not exist on type 'Type'. return arg; }
如果我們這樣做,編譯器會(huì)報(bào)錯(cuò),提示我們正在使用 arg
的 .length
屬性,但是我們卻沒有在其他地方聲明 arg
有這個(gè)屬性。我們前面也說了這些類型變量代表了任何甚至所有類型。所以完全有可能,調(diào)用的時(shí)候傳入的是一個(gè) number
類型,但是 number
并沒有 .length
屬性。
現(xiàn)在假設(shè)這個(gè)函數(shù),使用的是 Type
類型的數(shù)組而不是 Type
。因?yàn)槲覀兪褂玫氖菙?shù)組,.length
屬性肯定存在。我們就可以像創(chuàng)建其他類型的數(shù)組一樣寫:
function loggingIdentity<Type>(arg: Type[]): Type[] { console.log(arg.length); return arg; }
你可以這樣理解 loggingIdentity
的類型:泛型函數(shù) loggingIdentity
接受一個(gè) Type
類型參數(shù)和一個(gè)實(shí)參 arg
,實(shí)參 arg
是一個(gè) Type
類型的數(shù)組。而該函數(shù)返回一個(gè) Type
類型的數(shù)組。
如果我們傳入的是一個(gè)全是數(shù)字類型的數(shù)組,我們的返回值同樣是一個(gè)全是數(shù)字類型的數(shù)組,因?yàn)?nbsp;Type
會(huì)被當(dāng)成 number
傳入。
現(xiàn)在我們使用類型變量 Type
,是作為我們使用的類型的一部分,而不是之前的一整個(gè)類型,這會(huì)給我們更大的自由度。
我們也可以這樣寫這個(gè)例子,效果是一樣的:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> { console.log(arg.length); // Array has a .length, so no more error return arg; }
泛型類型 (Generic Types)
在上個(gè)章節(jié),我們已經(jīng)創(chuàng)建了一個(gè)泛型恒等函數(shù),可以支持傳入不同的類型。在這個(gè)章節(jié),我們探索函數(shù)本身的類型,以及如何創(chuàng)建泛型接口。
泛型函數(shù)的形式就跟其他非泛型函數(shù)的一樣,都需要先列一個(gè)類型參數(shù)列表,這有點(diǎn)像函數(shù)聲明:
function identity<Type>(arg: Type): Type { return arg; } let myIdentity: <Type>(arg: Type) => Type = identity;
泛型的類型參數(shù)可以使用不同的名字,只要數(shù)量和使用方式上一致即可:
function identity<Type>(arg: Type): Type { return arg; } let myIdentity: <Input>(arg: Input) => Input = identity;
我們也可以以對(duì)象類型的調(diào)用簽名的形式,書寫這個(gè)泛型類型:
function identity<Type>(arg: Type): Type { return arg; } let myIdentity: { <Type>(arg: Type): Type } = identity;
這可以引導(dǎo)我們寫出第一個(gè)泛型接口,讓我們使用上個(gè)例子中的對(duì)象字面量,然后把它的代碼移動(dòng)到接口里:
interface GenericIdentityFn { <Type>(arg: Type): Type; } function identity<Type>(arg: Type): Type { return arg; } let myIdentity: GenericIdentityFn = identity;
有的時(shí)候,我們會(huì)希望將泛型參數(shù)作為整個(gè)接口的參數(shù),這可以讓我們清楚的知道傳入的是什么參數(shù) (舉個(gè)例子:Dictionary<string>
而不是 Dictionary
)。而且接口里其他的成員也可以看到。
interface GenericIdentityFn<Type> { (arg: Type): Type; } function identity<Type>(arg: Type): Type { return arg; } let myIdentity: GenericIdentityFn<number> = identity;
注意在這個(gè)例子里,我們只做了少許改動(dòng)。不再描述一個(gè)泛型函數(shù),而是將一個(gè)非泛型函數(shù)簽名,作為泛型類型的一部分。
現(xiàn)在當(dāng)我們使用 GenericIdentityFn
的時(shí)候,需要明確給出參數(shù)的類型。(在這個(gè)例子中,是 number
),有效的鎖定了調(diào)用簽名使用的類型。
當(dāng)要描述一個(gè)包含泛型的類型時(shí),理解什么時(shí)候把類型參數(shù)放在調(diào)用簽名里,什么時(shí)候把它放在接口里是很有用的。
除了泛型接口之外,我們也可以創(chuàng)建泛型類。注意,不可能創(chuàng)建泛型枚舉類型和泛型命名空間。
泛型類(Generic Classes)
泛型類寫法上類似于泛型接口。在類名后面,使用尖括號(hào)中 <>
包裹住類型參數(shù)列表:
class GenericNumber<NumType> { zeroValue: NumType; add: (x: NumType, y: NumType) => NumType; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function (x, y) { return x + y; };
在這個(gè)例子中,并沒有限制你只能使用 number
類型。我們也可以使用 string
甚至更復(fù)雜的類型:
let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function (x, y) { return x + y; }; console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像接口一樣,把類型參數(shù)放在類上,可以確保類中的所有屬性都使用了相同的類型。
正如我們?cè)?Class 章節(jié)提過的,一個(gè)類它的類型有兩部分:靜態(tài)部分和實(shí)例部分。泛型類僅僅對(duì)實(shí)例部分生效,所以當(dāng)我們使用類的時(shí)候,注意靜態(tài)成員并不能使用類型參數(shù)。
泛型約束(Generic Constraints)
在早一點(diǎn)的 loggingIdentity
例子中,我們想要獲取參數(shù) arg
的 .length
屬性,但是編譯器并不能證明每種類型都有 .length
屬性,所以它會(huì)提示錯(cuò)誤:
function loggingIdentity<Type>(arg: Type): Type { console.log(arg.length); // Property 'length' does not exist on type 'Type'. return arg; }
相比于能兼容任何類型,我們更愿意約束這個(gè)函數(shù),讓它只能使用帶有 .length
屬性的類型。只要類型有這個(gè)成員,我們就允許使用它,但必須至少要有這個(gè)成員。為此,我們需要列出對(duì) Type
約束中的必要條件。
為此,我們需要?jiǎng)?chuàng)建一個(gè)接口,用來描述約束。這里,我們創(chuàng)建了一個(gè)只有 .length
屬性的接口,然后我們使用這個(gè)接口和 extend
關(guān)鍵詞實(shí)現(xiàn)了約束:
interface Lengthwise { length: number; } function loggingIdentity<Type extends Lengthwise>(arg: Type): Type { console.log(arg.length); // Now we know it has a .length property, so no more error return arg; }
現(xiàn)在這個(gè)泛型函數(shù)被約束了,它不再適用于所有類型:
loggingIdentity(3); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
我們需要傳入符合約束條件的值:
loggingIdentity({ length: 10, value: 3 });
在泛型約束中使用類型參數(shù)(Using Type Parameters in Generic Constraints)
你可以聲明一個(gè)類型參數(shù),這個(gè)類型參數(shù)被其他類型參數(shù)約束。
?
舉個(gè)例子,我們希望獲取一個(gè)對(duì)象給定屬性名的值,為此,我們需要確保我們不會(huì)獲取 obj
上不存在的屬性。所以我們?cè)趦蓚€(gè)類型之間建立一個(gè)約束:
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) { return obj[key]; } let x = { a: 1, b: 2, c: 3, d: 4 }; getProperty(x, "a"); getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
在泛型中使用類類型(Using Class Types in Generics)
在 TypeScript 中,當(dāng)使用工廠模式創(chuàng)建實(shí)例的時(shí)候,有必要通過他們的構(gòu)造函數(shù)推斷出類的類型,舉個(gè)例子:
function create<Type>(c: { new (): Type }): Type { return new c(); }
下面是一個(gè)更復(fù)雜的例子,使用原型屬性推斷和約束,構(gòu)造函數(shù)和類實(shí)例的關(guān)系。
class BeeKeeper { hasMask: boolean = true; } class ZooKeeper { nametag: string = "Mikle"; } class Animal { numLegs: number = 4; } class Bee extends Animal { keeper: BeeKeeper = new BeeKeeper(); } class Lion extends Animal { keeper: ZooKeeper = new ZooKeeper(); } function createInstance<A extends Animal>(c: new () => A): A { return new c(); } createInstance(Lion).keeper.nametag; createInstance(Bee).keeper.hasMask;
TypeScript 系列
TypeScript 中文手冊(cè) https://typescript.bootcss.com/
TypeScript 系列文章由官方文檔翻譯、重難點(diǎn)解析、實(shí)戰(zhàn)技巧三個(gè)部分組成,涵蓋入門、進(jìn)階、實(shí)戰(zhàn),旨在為你提供一個(gè)系統(tǒng)學(xué)習(xí) TS 的教程,更多關(guān)于TypeScript Generics泛型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript開發(fā)HapiJS應(yīng)用詳解
這篇文章主要為大家介紹了TypeScript開發(fā)HapiJS應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Manipulation-TypeScript?DOM操作示例解析
這篇文章主要為大家介紹了DOM?Manipulation-TypeScript?DOM操作示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03Webpack source map實(shí)戰(zhàn)分析詳解
這篇文章主要為大家介紹了Webpack source map示例分析詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12typescript快速上手的基礎(chǔ)知識(shí)篇
靜態(tài)類型的typescript與傳統(tǒng)動(dòng)態(tài)弱類型語(yǔ)言javascript不同,在執(zhí)行前會(huì)先編譯成javascript,因?yàn)樗鼜?qiáng)大的type類型系統(tǒng)加持,能讓我們?cè)诰帉懘a時(shí)增加更多嚴(yán)謹(jǐn)?shù)南拗?。注意,它并不是一門全新的語(yǔ)言,所以并沒有增加額外的學(xué)習(xí)成本2022-12-12