首先介紹一下泛型的概念
泛型程序設(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;
}
};
為了保證在對 sValue
和 nValue
的后續(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ù)類型和返回值類型:
我們也可以在調(diào)用的時(shí)候,小括號前顯示指定 T
代替的類型窿侈,比如 mdiddleware<string>(...)
炼幔,不過如果指定的類型與推導(dǎo)的類型有沖突,就會提示錯(cuò)誤:
泛型語法
泛型即可以聲明函數(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:
所以,泛型類其實(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泛型