前端深入理解Typescript泛型概念

泛型.jpg

首先介紹一下泛型的概念

泛型程序設(shè)計(jì)(generic programming)是程序設(shè)計(jì)語言的一種風(fēng)格或范式。泛型允許程序員在強(qiáng)類型程序設(shè)計(jì)語言中編寫代碼時(shí)使用一些以后才指定的類型轩拨,在實(shí)例化時(shí)作為參數(shù)指明這些類型栅螟。
泛型是指在定義函數(shù)紊婉,接口或者類的時(shí)候,不預(yù)先定義好具體的類型,而在使用的時(shí)候在指定類型的一種特性准浴。

一個(gè)小實(shí)例

我們來模擬一個(gè)場景:某個(gè)服務(wù)提供了一些不同類型的數(shù)據(jù)西傀,我們需要先通過一個(gè)中間件對這些數(shù)據(jù)進(jìn)行一個(gè)基本的處理(比如驗(yàn)證斤寇,容錯(cuò)等),再對其進(jìn)行使用拥褂。那么用 JavaScript 來寫應(yīng)該是這樣的

JavaScript 源碼

// 模擬服務(wù)娘锁,提供不同的數(shù)據(jù)。這里模擬了一個(gè)字符串和一個(gè)數(shù)值
var service = {
    getStringValue: function() {
        return "a string value";
    },
    getNumberValue: function() {
        return 20;
    }
};

// 處理數(shù)據(jù)的中間件饺鹃。這里用 log 來模擬處理莫秆,直接返回?cái)?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());

改寫成 TypeScript

先來看看對服務(wù)的改寫悔详,TypeScript 版的服務(wù)有返回類型:

const service = {
    getStringValue(): string {
        return "a string value";
    },

    getNumberValue(): number {
        return 20;
    }
};

為了保證在對 sValuenValue 的后續(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)出來归苍?

第 1 個(gè)辦法党巾,用 any

function middleware(value: any): any {
    console.log(value);
    return value;
}

上面這個(gè)辦法可以檢查通過。但它的問題在于 Any 類型會避開類型的檢查霜医,在后在對 Value 賦值的時(shí)候齿拂,也只是當(dāng)作類型沒有問題。簡單的說肴敛,是有“假裝”沒問題署海。所以假如輸入和輸出不是一樣的類型吗购,typescript也不會報(bào)錯(cuò)。

第 2 個(gè)辦法砸狞,多個(gè) middleware

function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }

當(dāng)然也可以用 TypeScript 的重載(overload)來實(shí)現(xiàn)

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 實(shí)現(xiàn)一樣沒有嚴(yán)格的類型檢查
}

這種方法最主要的一個(gè)問題是……如果我有 10 種類型的數(shù)據(jù)捻勉,就需要定義 10 個(gè)函數(shù)(或重載),那 20 個(gè)刀森,200 個(gè)呢……

正解:使用泛型(Generic)

現(xiàn)在我們切入正題踱启,用泛型來解決這個(gè)問題。那么這就需要解釋一下什么是泛型了:泛型就是指定一個(gè)表示類型的變量研底,用它來代替某個(gè)實(shí)際的類型用于編程埠偿,而后通過實(shí)際調(diào)用時(shí)傳入或推導(dǎo)的類型來對其進(jìn)行替換,以達(dá)到一段使用泛型程序可以實(shí)際適應(yīng)不同類型的目的榜晦。

雖然這個(gè)解釋已經(jīng)很接地氣了冠蒋,但是理解起來還是不如一個(gè)實(shí)例來得容易。我們來看看 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)出來是 string 類型斩郎,所以這個(gè)時(shí)候 T 代表了 string,因此此時(shí) middleware 的返回類型也就是 string喻频;而對于 middleware(getNumberValue()) 調(diào)用來說孽拷,這里的 T 表示了 number

我們直接從 VSCode 的提示可以看出來半抱,對于 middleware<T>() 調(diào)用脓恕,TypeScript 可以推導(dǎo)出參數(shù)類型和返回值類型:

image.png

我們也可以在調(diào)用的時(shí)候,小括號前顯示指定 T 代替的類型窿侈,比如 mdiddleware<string>(...)炼幔,不過如果指定的類型與推導(dǎo)的類型有沖突,就會提示錯(cuò)誤:

image.png

泛型語法

泛型即可以聲明函數(shù), 也可以聲明. 也可以聲明接口

class Person<T>{} // 一個(gè)尖括號跟在類名后面
function Person<T>(arg: T): T {return arg;}  // 一個(gè)尖括號跟在函數(shù)名后面
interface Person<T> {}  // 一個(gè)尖括號跟在接口名后面

多個(gè)類型參數(shù)

我們在定義范型的時(shí)候史简,也可以一次定義多個(gè)類型參數(shù)乃秀,像下面這樣。

function swap<T, U>(tuple: [T, U]):[U, T] {
    return [tuple[1], tuple[0]];
}

泛型接口

我們先定義一個(gè)范型接口Identities,然后定義一個(gè)函數(shù)identities()來使用這個(gè)范型接口

interface Identities<T, U> {
    id1: T;
    id2: U;
}

我在這里使用T和U作為我們的類型變量來演示任何字母(或有效的字母數(shù)字名稱的組合)都是有效的類型—除了常規(guī)用途之外圆兵,您對它們的調(diào)用沒有任何意義跺讯。
我們現(xiàn)在可以將這個(gè)接口應(yīng)用為identity()的返回類型,修改我們的返回類型以符合它殉农。我們還可以console.log這些參數(shù)和它們的類型刀脏,以便進(jìn)一步說明:

function identities<T, U> (arg1: T, arg2: U): Identities<T, U> {
   console.log(arg1 + ": " + typeof (arg1));
   console.log(arg2 + ": " + typeof (arg2));
   let identities: Identities<T, U> = {
    id1: arg1,
    id2: arg2
  };
  return identities;
}

我們現(xiàn)在對identity()所做的是將類型T和U傳遞到函數(shù)和identity接口中,從而允許我們定義與參數(shù)類型相關(guān)的返回類型超凳。

范型變量

使用泛型創(chuàng)建像identity這樣的泛型函數(shù)時(shí)愈污,編譯器要求你在函數(shù)體必須正確的使用這個(gè)通用的類型耀态。 換句話說,你必須把這些參數(shù)當(dāng)做是任意或所有類型暂雹。
我們先看下之前例子

function genericDemo<T>(data: T):T {
    return data;
}

如果我們想同時(shí)打印出data的長度首装。 我們很可能會這樣做

function genericDemo<T>(data: T):T {
    console.log(data.length); // Error: T doesn't have .length
    return data;
}

如果這么做,編譯器會報(bào)錯(cuò)說我們使用了data的.length屬性杭跪,但是沒有地方指明data具有這個(gè)屬性仙逻。 記住,這些類型變量代表的是任意類型涧尿,所以使用這個(gè)函數(shù)的人可能傳入的是個(gè)數(shù)字系奉,而數(shù)字是沒有.length屬性的。
現(xiàn)在假設(shè)我們想操作T類型的數(shù)組而不直接是T现斋。由于我們操作的是數(shù)組喜最,所以.length屬性是應(yīng)該存在的偎蘸。 我們可以像創(chuàng)建其它數(shù)組一樣創(chuàng)建這個(gè)數(shù)組:

function genericDemo<T>(data: Array<T>):Array<T> {
    console.log(data.length);
    return data;
}

范型類

前面已經(jīng)解釋了“泛型”這個(gè)概念庄蹋。示例中泛型的用法我們稱之為“泛型函數(shù)”。不過泛型更廣泛的用法是用于“泛型類”——即在聲明類的時(shí)候聲明泛型迷雪,那么在類的整個(gè)作用域范圍內(nèi)都可以使用聲明的泛型類型限书。

相信大家都已經(jīng)對數(shù)組有所了解,比如 string[] 表示字符串?dāng)?shù)組類型章咧。其實(shí)在早期的 TypeScript 版本中沒有這種數(shù)組類型表示倦西,而是采用實(shí)例化的泛型 Array<string> 來表示的,現(xiàn)在仍然可以使用這方式來表示數(shù)組赁严。

除此之外扰柠,TypeScript 中還有一個(gè)很常用的泛型類,Promise<T>疼约。因?yàn)?Promise 往往是帶數(shù)據(jù)的卤档,所以通過 Promise<T> 這種泛型定義的形式,可以表示一個(gè) Promise 所帶數(shù)據(jù)的類型程剥。比如下圖就可以看出劝枣,TypeScript 能正確推導(dǎo)出 n 的類型是 number:


image.png

所以,泛型類其實(shí)多數(shù)時(shí)候是應(yīng)用于容器類织鲸。假設(shè)我們需要實(shí)現(xiàn)一個(gè) FilteredList舔腾,我們可以向其中 add()(添加) 任意數(shù)據(jù),但是它在添加的時(shí)候會自動過濾掉不符合條件的一些搂擦,最終通過 get all() 輸出所有符合條件的數(shù)據(jù)(數(shù)組)稳诚。而過濾條件在構(gòu)造對象的時(shí)候,以函數(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 聲明為一個(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 或別的什么工扎,但是一般建議以大寫的 T 作為前綴,采用 Pascal 命名規(guī)則衔蹲,方便識別肢娘。還有一些常見的指代,比如 TKey 表示鍵類型舆驶,TValue 表示值類型等(常用于映射表這類容器定義)橱健。

我們還可以在類屬性和方法的意義上使類泛型。泛型類確保在整個(gè)類中一致地使用指定的數(shù)據(jù)類型沙廉。例如下面這種在React Typescript項(xiàng)目中的寫法拘荡。

interface Props {
    className?: string;
    ...
}

interface State {
    submitted?: bool;
    ...
}

class MyComponent extends React.Component<Props, State> {
   ...
}

我們在這里使用與React組件一起使用的泛型,以確保組件的props和state是類型安全的撬陵。

泛型約束

我們先看一個(gè)常見的需求珊皿,我們要設(shè)計(jì)一個(gè)函數(shù),這個(gè)函數(shù)接受兩個(gè)參數(shù)巨税,一個(gè)參數(shù)為對象蟋定,另一個(gè)參數(shù)為對象上的屬性,我們通過這兩個(gè)參數(shù)返回這個(gè)屬性的值草添,比如:

function getValue(obj: object, key: string){
    return obj[key] // error
}

我們會得到一段報(bào)錯(cuò)驶兜,這是新手 TypeScript 開發(fā)者常常犯的錯(cuò)誤,編譯器告訴我們远寸,參數(shù) obj 實(shí)際上是 {},因此后面的 key 是無法在上面取到任何值的抄淑。
因?yàn)槲覀兘o參數(shù) obj 定義的類型就是 object,在默認(rèn)情況下它只能是 {}驰后,但是我們接受的對象是各種各樣的肆资,我們需要一個(gè)泛型來表示傳入的對象類型,比如T extends object:

function getValue<T extends object>(obj: T, key: string) {
  return obj[key] // error
}

這依然解決不了問題倡怎,因?yàn)槲覀兊诙€(gè)參數(shù) key 是不是存在于 obj 上是無法確定的迅耘,因此我們需要對這個(gè) key 也進(jìn)行約束,我們把它約束為只存在于 obj 屬性的類型监署,這個(gè)時(shí)候需要借助到后面我們會進(jìn)行學(xué)習(xí)的索引類型進(jìn)行實(shí)現(xiàn) <U extends keyof T>颤专,我們用索引類型 keyof T 把傳入的對象的屬性類型取出生成一個(gè)聯(lián)合類型,這里的泛型 U 被約束在這個(gè)聯(lián)合類型中钠乏,這樣一來函數(shù)就被完整定義了:

function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
  return obj[key] // ok
}

泛型在Http接口中的應(yīng)用

在實(shí)際項(xiàng)目中, 每個(gè)項(xiàng)目都需要接口請求, 我們會封裝一個(gè)通用的接口請求, 在這個(gè)函數(shù)里面, 處理一些常見的錯(cuò)誤等等. 為了讓每個(gè)接口調(diào)用都有 typescript 約束, 提醒. 這里使用泛型是非常合適了.
假如在項(xiàng)目開發(fā)前期栖秕,前后端規(guī)定的接口數(shù)據(jù)格式為:

{
  code: 200,
  message: "",
  data: {}
}

所有的接口都遵從這樣的格式。

  • code 代表接口的成功與失敗
  • message 代表接口失敗之后的服務(wù)端消息輸出
  • data 代表接口成功之后真正的邏輯

在 ajax.d.ts 文件使用泛型定義了一個(gè) response 的類型晓避,如:

// 使用枚舉定義常量
export enum StateCode {
  error = 400,
  ok = 200,
  timeout = 408,
  serviceError = 500
}
// 定義了一個(gè) response 的類型
export interface IResponse<T = any> {
  code: StateCode;
  message: string;
  data: T;
}

接著簇捍,在使ajax.d.ts中定義好返回值data的數(shù)據(jù)類型:

export interface IFavorites {
  id: string;
  img: string;
  name: string;
  url: string;
}

然后在請求的時(shí)候就能進(jìn)行使用

this.axiosRequest({ key: 'idc' }).then((response: IResponse<IFavorites>) => {
 const { code, message, data} = response;
  if (code === StateCode.ok) {
    // 處理 
  }
})

站在巨人肩上

前端深入理解Typescript泛型概念
從 JavaScript 到 TypeScript - 泛型
大話 Typescript泛型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末只壳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子暑塑,更是在濱河造成了極大的恐慌吼句,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件事格,死亡現(xiàn)場離奇詭異惕艳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)驹愚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門远搪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逢捺,你說我怎么就攤上這事谁鳍。” “怎么了劫瞳?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵倘潜,是天一觀的道長。 經(jīng)常有香客問我柠新,道長窍荧,這世上最難降的妖魔是什么辉巡? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任恨憎,我火速辦了婚禮,結(jié)果婚禮上郊楣,老公的妹妹穿的比我還像新娘憔恳。我一直安慰自己,他們只是感情好净蚤,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布钥组。 她就那樣靜靜地躺著,像睡著了一般今瀑。 火紅的嫁衣襯著肌膚如雪程梦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天橘荠,我揣著相機(jī)與錄音屿附,去河邊找鬼。 笑死哥童,一個(gè)胖子當(dāng)著我的面吹牛挺份,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贮懈,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼匀泊,長吁一口氣:“原來是場噩夢啊……” “哼优训!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起各聘,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤揣非,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后躲因,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妆兑,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年毛仪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搁嗓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡箱靴,死狀恐怖腺逛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衡怀,我是刑警寧澤棍矛,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站抛杨,受9級特大地震影響够委,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜怖现,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一茁帽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧屈嗤,春花似錦潘拨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茫船,卻和暖如春琅束,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背算谈。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工涩禀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人濒生。 一個(gè)月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓埋泵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子丽声,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350