第七節(jié): TypeScript 泛型

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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末局劲,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子奶赠,更是在濱河造成了極大的恐慌鱼填,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅戈,死亡現(xiàn)場離奇詭異苹丸,居然都是意外死亡,警方通過查閱死者的電腦和手機竹祷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門谈跛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人塑陵,你說我怎么就攤上這事感憾。” “怎么了令花?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵阻桅,是天一觀的道長。 經(jīng)常有香客問我兼都,道長嫂沉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任扮碧,我火速辦了婚禮趟章,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘慎王。我一直安慰自己蚓土,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布赖淤。 她就那樣靜靜地躺著蜀漆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咱旱。 梳的紋絲不亂的頭發(fā)上确丢,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天绷耍,我揣著相機與錄音,去河邊找鬼鲜侥。 笑死褂始,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的描函。 我是一名探鬼主播病袄,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赘阀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起脑奠,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤基公,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宋欺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轰豆,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年齿诞,在試婚紗的時候發(fā)現(xiàn)自己被綠了酸休。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡祷杈,死狀恐怖斑司,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情但汞,我是刑警寧澤宿刮,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站私蕾,受9級特大地震影響僵缺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜踩叭,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一磕潮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧容贝,春花似錦自脯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茂缚,卻和暖如春戏罢,著一層夾襖步出監(jiān)牢的瞬間屋谭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工龟糕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桐磁,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓讲岁,卻偏偏與公主長得像我擂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缓艳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344