1. 泛型理解
泛型是通過參數(shù)化類型來實現(xiàn)在同一份代碼上操作多種數(shù)據(jù)類型。
1.1 未使用泛型
現(xiàn)在如果需要實現(xiàn)函數(shù)接受什么類型的數(shù)據(jù)就返回什么類型的數(shù)據(jù),在不使用泛型的情況下,代碼如下
function identity(num:number):number{
return num
}
上面的示例只能實現(xiàn)函數(shù)接受number
類型的實參,并且返回number
類型, 沒法處理其他數(shù)據(jù)類型
也就是說如果想實現(xiàn)一個函數(shù)接受一個string
類型參數(shù), 并返回string
類型, 上面的函數(shù)不能使用
當(dāng)然我們也可以選擇任意數(shù)據(jù)類型any,
代碼如下
function identity(arg: any): any {
return arg
}
雖然感覺起來能夠?qū)崿F(xiàn)我們的需求, 函數(shù)傳遞什么數(shù)據(jù)類型, 就返回什么數(shù)據(jù)類型的值.
但是any類型會導(dǎo)致這個函數(shù)會丟失了一些信息:傳入的類型與返回的類型并不一定相同的邑遏。
例如: 將代碼改寫一下
function identity(arg: any): any {
return arg + ''
}
這樣的寫法如果傳入number
類型 返回string
類型, 編譯的時候并不會報錯,因為any類型可以是任意數(shù)據(jù)類型, 沒有規(guī)定參數(shù)的any類型 和返回值any類型 必須保持一致.
1.2 使用泛型
我們需要一種捕獲參數(shù)類型的方法另玖,以便我們也可以使用它來表示返回的內(nèi)容困曙。在這里表伦,我們將使用類型變量,一種特殊類型的變量慷丽,它作用于類型而不是值, 這就是我們所謂的泛型
簡單說就像我們聲明的變量或函數(shù)參數(shù)一樣, 只不過變量和參數(shù)是用來接受值, 而泛型的參數(shù)類型是用來接受類型的
例如: 使用泛型實現(xiàn)上述功能
function identity<T>(arg: T): T {
return arg
}
此時,通過參數(shù)類型T
將參數(shù)類型和返回類型建立了關(guān)系,
向函數(shù)添加了一個類型變量:T
, 這里的T
只是一個變量,用于捕獲用戶提供的類型, 以便我們可以使用該信息.
此時T
再次使用作為返回類型, 此時函數(shù)的參數(shù)和返回類型使用了相同類型T
, 這樣Type
捕獲到用戶傳入的是什么類型, 那么函數(shù)返回的也將是什么類型
這時我么也可以說identity
函數(shù)是一個通用函數(shù), 因為它適用于多種類型.
1.3 泛型的調(diào)用
一旦我們編寫了通用標(biāo)識函數(shù)蹦哼,我們就可以通過兩種方式之一調(diào)用它
第一種方法是將所有的參數(shù)(包括參數(shù)類型)傳遞給函數(shù)
例如:
// <>中的Type 是一個類型變量
function identity<Type>(arg:Type):Type{
return arg
}
// <> 中的類型是明確告知函數(shù),此時調(diào)用時類型變量所代表的類型
const x = identity<string>('hello')
console.log(x)
// const x:string
在這里,我們明確的將Type
設(shè)置為string
類型, 表示函數(shù)的參數(shù)和返回值都是string類型
類型變量和傳遞明確類型使用<>
而不是()
第二種方式也是最常見的,就是使用TypeScript的類型參數(shù)推斷,
也就是說,我們不明確指定Type
的類型,而是希望TypeScript編譯器根據(jù)我們傳入的參數(shù)類型自動推斷并設(shè)置Type
的類型,
例如:
function identity<Type>(arg:Type):Type{
return arg
}
const x = identity('hello')
console.log(x)
// const x:string
這里并不必在尖括號(<>)中顯示的傳遞類型, 編譯器只是查看參數(shù)'hello'
值并推斷值的類型,然后將Type
設(shè)置為此類型
雖然類型推斷可以成為保持代碼更短和更具可讀性的有用工具, 但是當(dāng)編譯器無法推斷類型時, 就可能需要我們使用方法一的方式, 顯示的傳遞類型參數(shù).
2. 使用泛型參數(shù)屬性
當(dāng)你開始使用泛型時, 你會注意到, 當(dāng)你創(chuàng)建像identity
這類泛型函數(shù)時, 編譯器將會強制你正確的使用函數(shù)中任何泛型類型參數(shù), 也就是說,你實際上將這些參數(shù)看做為可以是任何類型,因為類型參數(shù)可以接收任意類型
我們依然使用之前的identity
函數(shù), 但此時我們還想在arg
參數(shù)每次調(diào)用時將參數(shù)的長度打印到控制臺,我們可能會想這樣處理
function identity<Type>(arg:Type):Type{
console.log(arg.length)
// Type類型上不存在length屬性
return arg
}
當(dāng)我們這樣做的時候,TypeScript就會給我們一個錯誤, 告訴我們Type
類型上沒有length
屬性.
請記住,我們之前說過這些類型變量,如Type
代表任何類型, Type
的具體類型,取決于調(diào)用時傳遞的類型或指定的類型
因此在使用函數(shù)的人很有可能傳入number
類型, 而number
類型的值是沒有length
屬性的
const x = identity<number>(20)
假設(shè)我們此時打算讓這個函數(shù)作用于數(shù)組Type
(就是數(shù)組中每一項是Type類型), 而不是Type
類型直接作用于數(shù)組(就是Type類型就是數(shù)組本身).
簡單說,我們希望的是 Type[]
指代number[]
或者string[]
, 也有可能是其他類型數(shù)組, 而此時Type
的類型可能是number
,string
我們不希望的是Type
直接指代number[]
由于我們真正使用數(shù)組,因此參數(shù)的length
屬性就可以使用了,
那么我們就可以像創(chuàng)建其他類型數(shù)組一樣來描述它:
例如:
function identity<Type>(arg:Type[]):Type[]{
console.log(arg.length)
return arg
}
此時你可以將類型解讀泛型函數(shù)identity
接受一個類型參數(shù)Type
, 和一個普通的函數(shù)參數(shù), 該參數(shù)arg
是一個Type
類型的數(shù)組, 并返回一個Type
類型的數(shù)組.
比如,我們給函數(shù)參數(shù)傳入一個數(shù)字?jǐn)?shù)組(即number[])
, 我們也將返回一個number[]
此時Type
類型綁定到了number
類型, 這表示TypeScript允許我們將泛型的類型變量Type
用作我們正在使用類型的一部分, 而不是整個類型,從而為我們提供了更大的靈活性
上述的示例我們也可以如下編寫
function identity<Type>(arg:Array<Type>):Array<Type>{
console.log(arg.length)
return arg
}
其實就是函數(shù)類型的寫法以及簡寫方式
3. 泛型類型
在前面的部分中,我們創(chuàng)建了適用于一系列類型的通用標(biāo)識函數(shù)要糊。在本節(jié)中纲熏,我們將探討函數(shù)本身的類型以及如何創(chuàng)建通用接口。
泛型函數(shù)的類型和非泛型函數(shù)的類型 一樣, 類型參數(shù)先列出來, 類似于函數(shù)聲明
// 泛型函數(shù)
function identity<Type>(arg:Type):Type{
return arg
}
// 使用泛型函數(shù)的類型來對變量進行類型注釋
let myIdentity:<Type>(arg:Type) => Type = identity
我們也可以為類型中的泛型類型參數(shù)使用不同的名稱, 只要類型變量的數(shù)量和類型變量的使用方式保持一致
// 泛型函數(shù)
function identity<Type>(arg:Type):Type{
return arg
}
// 使用泛型函數(shù)的類型來對變量進行類型注釋
let myIdentity:<Input>(arg:Input) => Input = identity
從本質(zhì)上來將, Type
也好, Input
也好其實就是類型的變量而已, 就像我們定義的普通變量一樣, 不管定義什么名字, 只要保證使用的地方對了就行了
例如示例中,<Input>(arg:Input) => Input
只是對于變量myIdentity
做類型注釋,表面myIdentity
是一個函數(shù),
并且函數(shù)的參數(shù)類型,返回值類型 使用泛型類型變量, 也就是類型保持同步,
我們還可以將泛型類型注釋改寫成對象字面量類型的調(diào)用簽名, 這個調(diào)用簽名前面章節(jié)已經(jīng)了解過了
// 泛型函數(shù)
function identity<Type>(arg:Type):Type{
return arg
}
// 使用對象字面量類型的調(diào)用簽名進行類型注釋
let myIdentity:{<Type>(arg:Type):Type} = identity
此時我們也可以將對象 字面量類型移動 到接口中,此時就成了通用接口
// 通用接口
// 接口中定義調(diào)用簽名
interface GenericIdentity{
<Type>(arg:Type):Type
}
// 泛型函數(shù)
function identity<Type>(arg:Type):Type{
return arg
}
// 使用通用接口進行類型注釋
let myIdentity:GenericIdentity = identity
在實例中,我們也可以將泛型參數(shù)移動為整個接口的參數(shù), 這讓我們看到了泛型的類型,對于整個接口的其他成員都是可見的
// 通用接口
// 接口中定義調(diào)用簽名
// 將泛型參數(shù)移動為接口參數(shù)
interface GenericIdentity<Type>{
(arg:Type):Type
}
// 泛型函數(shù)
function identity<Type>(arg:Type):Type{
return arg
}
// 使用通用接口進行類型注釋
// 此時在使用通用接口進行類型注釋的時候就需要傳遞接口類型參數(shù)
let myIdentity:GenericIdentity<number> = identity
請注意,此時 我們的示例已經(jīng)被更改的略有不同, 我們現(xiàn)在有了一個非泛型函數(shù)簽名, 他是泛型接口的一部分, 而不是描述泛型函數(shù)
當(dāng)我們使用GenericIdentity
時, 我們還需要制定相應(yīng)的類型參數(shù),示例中傳入了number
類型
從而有效的鎖定底層調(diào)用簽名時將要使用的內(nèi)容.
我們 需要了解,何時將類型參數(shù)放在調(diào)用簽名上, 何時將其放 接口上, 放在什么位置有助于描述類型在那些方面
除了泛型接口外,我們還可以創(chuàng)建泛型類
4. 泛型類
泛型類具有與泛型接口相似的形狀, 泛型類在類的名稱后面的尖括號(<>
)中有一個泛型類型參數(shù)列表
例如
// 通用類
class GenericNumber<NumType>{
value: NumType
add:(x:NumType,y:NumType) => NumType
}
// 使用通用類
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.value = 30
myGenericNumber.add = function(x,y){
return x + y
}
myGenericNumber.add(10,20)
此時我們在 使用GenericNumber
類時,沒有什么限制它只能使用number
類型, 我們也可以使用string
類型,或更加復(fù)雜的對象
// 通用類使用字符串類型
let myGenericNumber = new GenericNumber<string>()
myGenericNumber.value = 'hello'
myGenericNumber.add = function(x,y){
return x + y
}
myGenericNumber.add('hello ','world')
請注意, 正如class類中介紹的那樣, 一個類的類型有兩方面:靜態(tài)方面和實例方面, 泛型類僅在其實例方面而非其靜態(tài)方面是通用的
因此要注意在使用類時, 靜態(tài)成員不能使用類的類型參數(shù)
5. 通用約束
如果我們要編寫了適用于一組類型的泛型函數(shù), 你知道該組函數(shù)將具有哪些功能, 我們在示例中希望能夠訪問參數(shù)的length
屬性, 但是編譯器無法證明每種類型都有length
屬性, 因此,TypeScript報錯
function identity<Type>(arg:Type):Type{
console.log(arg.length)
// 警告, Type上不具有l(wèi)ength屬性
return arg
}
我們不想使用任何類型, 而希望將此函數(shù)限制為使用具有length
屬性的任何類型,
只要類型有了這個屬性,至少 擁有這個屬性, 我們就會允許它通過類型效驗
為此我們必須將我們的要求列為Type
的限制條件
我們將創(chuàng)建一個具有length
屬性的約束接口,然后我們使用extends
關(guān)鍵詞繼承該接口, 以此來表示我們的約束
例如:
// 具有l(wèi)ength屬性的接口
interface Lengthwish{
length:number
}
// 通過泛型類型參數(shù)extends繼承具有l(wèi)ength屬性接口
// 此時調(diào)用函數(shù)傳入的任意類型必須滿足具有l(wèi)ength屬性, 以此達到約束的目的
function identity<Type extends Lengthwise>(arg:Type):Type{
console.log(arg.length)
return arg
}
因為泛型函數(shù)此時受到了接口的約束, 它將不再適用于任何類型
// 調(diào)用函數(shù)警告
identity(3);
// 警告: 類型number不能賦給具有Lenthwise類型的參數(shù)
相反的,我們傳入?yún)?shù)的類型必須具有所有的必需屬性的值
identity({length:10,value:3});
6 在泛型約束中使用類型參數(shù)
其實所謂的泛型約束就是通過extends
關(guān)鍵字, 繼承來達到, 傳入的類型必須滿足extends后面的約束
而泛型約束中使用類型參數(shù), 就是extends
關(guān)鍵字后面的約束條件中將使用另外一個類型參數(shù)
例如:我們想從一個給定名稱的對象上獲取一個屬性, 我們想確保我們不會獲取到對象上不存在的屬性
我們將在兩種類型參數(shù)之間放置一個約束條件
示例:
// 約束類型參數(shù)Key
function getProperty<Type,Key extends keyof Type>(obj:Type,key:Key){
return obj[key]
}
const x = {a:1,b:2,c:3}
getProperty(x,'a')
getProperty(x,'m')
// 警告:類型參數(shù)'m'不能賦值給'a'|'b'|'c'類型;
實例中keyof
關(guān)鍵字是TypeScript提供的用于獲取對象類型鍵值組成的文字聯(lián)合類型
例如:
interface Person {
name:string;
age: number;
}
type Num = keyof Person;
/*
鼠標(biāo)移入Num看到的類型: type Num = keyof Person
其實Num的類型 是 Person類型key 組成的文字聯(lián)合類型
也就是 'name' | 'age'
*/
// 條件判斷 'name' 文字類型是否是Num類型的子類型, 是返回string類型, 否返回boolean類型
type A = 'name' extends Num ? string: boolean
// type Num = keyof Person
理解了keyof
關(guān)鍵字的作用, 前面的例子就好理解了
7. 在泛型中使用類類型
在TypeScript 中使用泛型創(chuàng)建工廠函數(shù)時, 我們需要通過其構(gòu)造函數(shù)引用類類型,
例如:
// 泛型函數(shù)
// 泛型T 是類返回的對象
function create<T>(c: {new(name:string,age:number):T}):T{
return new c('張三',18)
}
// 類
class Person{
name:string
age: number
constructor(name:string,age:number){
this.name = name;
this.age = age
}
}
let student = create(Person)
console.log('student', student)
一個更高級的示例使用原型屬性來推斷和約束構(gòu)造函數(shù)和類類型的實例端之間的關(guān)系锄俄。
// BeeKeeper類
class BeeKeeper {
hasMask: boolean = true;
}
// ZooKeeper類
class ZooKeeper {
nametag: string = "Mikle";
}
// Animal類
class Animal {
numLegs: number = 4;
}
// Bee類繼承 Animal類
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
// Lion 類繼承 Animal類
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
// 函數(shù)接受一個類, 這個類的實例化后返回的A 要繼承 Animal類
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
// function createInstance<Lion>(c: new () => Lion): Lion
createInstance(Bee).keeper.hasMask;
// function createInstance<Bee>(c: new () => Bee): Bee