typescript類型體操及關(guān)鍵字使用示例詳解
前言
事情是這樣的,有這樣一道 ts 類型題,代碼如下所示:
type Union = "Mon" | "Tue" | "Wed"; // 補(bǔ)充這里的類型代碼 type Mapping<T extends Union, Value extends string> = any; type Res = Mapping<Union, "周一" | "周二" | "周三">; // 以下是輸出結(jié)果 // { // mon: "周一"; // Tue: "周二"; // Wed: "周三"; // }
觀察題目,其實(shí)就是將兩個聯(lián)合類型的值組合成接口,其中第一個聯(lián)合類型的值作為屬性,第二個聯(lián)合類型的值則作為屬性值,并且兩者的屬性順序是一一對應(yīng)的。下面跟著我一起來分析,通過這道題,我們能理解到 ts 的不少知識點(diǎn),不信繼續(xù)往下看。
分析
實(shí)際上,在 ts 當(dāng)中,想要保證 ts 的順序是很困難的,這與 ts 編譯器有關(guān),不過這不影響我們對這道題的分析,那么這道題如何解決呢?思路就是想辦法將 2 個聯(lián)合類型構(gòu)造成數(shù)組,然后就可以根據(jù)數(shù)組項(xiàng)一一對應(yīng)來轉(zhuǎn)成對象了,那么這道題的難點(diǎn)在于如何轉(zhuǎn)成數(shù)組。
轉(zhuǎn)成數(shù)組的前提就是我們將聯(lián)合類型的每一項(xiàng)取出來然后添加到數(shù)組中,那么如何提取呢?下面讓我們一步一步來實(shí)現(xiàn)。
將并集轉(zhuǎn)成交集
聯(lián)合類型我們也可以叫做并集,如: 1 | 2 | 3
,而要實(shí)現(xiàn)添加的第一步,我們需要將并集轉(zhuǎn)成交集,那么如何進(jìn)行交集的轉(zhuǎn)換呢?
其實(shí)我們可以將并集的每一項(xiàng)使用函數(shù)來推斷,在這里我們需要理解 ts 中的 2 個關(guān)鍵字用法,如下:
- extends: 既可以表示類的繼承,也可以表示條件判斷(相當(dāng)于 js 的全等)。
- infer: 該關(guān)鍵字用于推導(dǎo)某個類型。
根據(jù)以上分析,我們就可以實(shí)現(xiàn)并集轉(zhuǎn)成交集,代碼如下:
// X | Y | Z ==> X & Y & Z type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( k: infer I ) => void ? I : never;
以上類型就實(shí)現(xiàn)了并集對交集的轉(zhuǎn)換,理解起來也很容易,就是判斷給定的泛型參數(shù)是否是任意類型 any,如果是則構(gòu)造成函數(shù)參數(shù)為該類型,然后使用 infer 關(guān)鍵字去推導(dǎo)參數(shù)類型,如果能推導(dǎo)出來,則返回推導(dǎo)出來的結(jié)果,否則返回 never。
any 與 never 與 void 類型
這個 ts 類型也涉及到了 3 個類型,即任意類型 any,從不類型 never,和 void 類型。
any 類型
其中 any 用來表示允許賦值為任意類型。在 ts 中,如果是一個普通類型,在賦值過程中改變類型是不被允許的。例如:
let a: string = "123"; a = 2; // error TS2322: Type 'number' is not assignable to type 'string'
以上定義 a 變量的類型是 string,因此修改變量值為數(shù)值,則 ts 編譯會出錯,但如果是賦值為任意值類型,則以上操作不會報錯,如下所示:
let a: any = "123"; a = 2; // 允許修改,因?yàn)槭侨我庵殿愋?/pre>
我們也可以訪問任意類型的屬性和方法,如下所示:
let b: any = "b"; console.log(b.name); // ts編譯不會報錯 console.log(b.setName("a")); // ts編譯不會出錯
也就是說,聲明一個變量為任意值之后,對它的任何操作,返回的內(nèi)容的類型都是任意值。
在 ts 中,一個未聲明類型的變量,也會被推導(dǎo)成任意類型,如:
let a; a = "a"; a = 2; a.setName("b"); // 以上操作在ts中都不會報錯
never 類型
never 類型表示從不存在的類型,比如一個函數(shù)拋出異常,它的返回類型就是 never,如:
const fn = (msg: string) => throw new Error(msg); // never
void 類型
void 類型表示沒有返回值,通常用在沒有任何返回值的函數(shù)中。如:
const fn = (): void => { alert(123); };
以上類型是包裝成函數(shù)類型推導(dǎo),對于函數(shù)有沒有返回值沒有任何意義,因此這里只需要使用 void 來代表返回值即可。
將聯(lián)合類型轉(zhuǎn)換成重載函數(shù)類型
下一步,我們就需要將聯(lián)合類型轉(zhuǎn)換成重載函數(shù)類型,例如:
X | Y ==> ((x: X)=>void) & ((y:Y)=>void)
我們要如何實(shí)現(xiàn)呢?其實(shí)就是將泛型參數(shù)包裝成函數(shù)類型,然后再調(diào)用用前面的并集轉(zhuǎn)交集類型,如下:
type UnionToOvlds<U> = UnionToIntersection< U extends any ? (f: U) => void : never >;
做這一步的目的是方便將聯(lián)合類型中的每一項(xiàng)提取出來,因此需要這個類型,接下來我們就需要將聯(lián)合類型的每一項(xiàng)取出來,我們叫做 PopUnion 類型。
從聯(lián)合類型中取出每一個類型
有了前面 2 個類型的鋪墊,取出聯(lián)合類型中的每一個類型就很容易,我們只需要包裝成重載函數(shù)類型,然后使用 infer 推斷函數(shù)參數(shù)類型,返回參數(shù)類型即可。代碼如下:
type PopUnion<U> = UnionToOvlds<U> extends (f: infer A) => void ? A : never;
能夠取出聯(lián)合類型的每一個類型,那么構(gòu)造成數(shù)組就很容易了,不過接下來還需要一個類型,那就是判斷是否是聯(lián)合類型,為此我們需要先實(shí)現(xiàn)這個類型,即 IsUnion 類型。
判斷是否是聯(lián)合類型
判斷是否是聯(lián)合類型比較簡單,就是將類型構(gòu)造成一個數(shù)組,然后使用兩個數(shù)組比較,不過我們需要比較的是原始泛型參數(shù)構(gòu)造成數(shù)組和轉(zhuǎn)成交集構(gòu)造成數(shù)組是否相等,相等則返回 false,否則返回 true。代碼如下:
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
聯(lián)合類型轉(zhuǎn)數(shù)組
接下來就是聯(lián)合類型轉(zhuǎn)數(shù)組類型的實(shí)現(xiàn),首先我們需要用到上一節(jié)提到的判斷是否是聯(lián)合類型,如果是聯(lián)合類型,則使用 PopUnion 類型提取聯(lián)合類型的每一項(xiàng),注意這里是需要遞歸的提取剩余項(xiàng)的,直到不是剩余項(xiàng)不是聯(lián)合類型為止,因此這里我們沒提取一項(xiàng),都需要使用 Exclude 類型將聯(lián)合類型中提取的排除掉,這樣就得到了剩余的聯(lián)合類型,然后我們使用第二個參數(shù)來存儲結(jié)果,如果不是聯(lián)合類型就直接添加到數(shù)組中。代碼如下所示:
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]> : [T, ...A];
以上代碼我們使用泛型 A 是一個未知類型的數(shù)組,并默認(rèn)賦值為空數(shù)組來存儲結(jié)果,相當(dāng)于我們是從聯(lián)合類型當(dāng)中一項(xiàng)一項(xiàng)的提取出來然后添加到 A 結(jié)果數(shù)組中,最終返回的結(jié)果就是一個由聯(lián)合類型每一項(xiàng)組成的數(shù)組。
Exclude 類型
其中 Exclude 類型是 ts 內(nèi)置類型,不過要實(shí)現(xiàn)還是比較簡單的,簡單來說就是如果兩個參數(shù)相等,則不返回類型,否則返回原類型。代碼如下:
type Exclude<T, U> = T extends U ? never : T;
獲取數(shù)組的長度
接下來我們還需要比較兩個聯(lián)合類型提取出來的數(shù)組長度是否相同,為此我們需要先實(shí)現(xiàn)如何獲取一個數(shù)組類型的長度,觀察發(fā)現(xiàn)數(shù)組是存在一個 length 屬性的,因此我們可以判斷如果存在 length 屬性,并使用 infer 推斷具體值,能夠推斷出來就返回這個推斷的值,否則返回 never,代碼如下:
type Length<T extends ReadonlyArray<any>> = T extends { length: infer L } ? L : never;
這個代碼有一個類型即 ReadonlyArray 類型,它也是 ts 的一個內(nèi)置類型,表示數(shù)組項(xiàng)只讀的數(shù)組,那么這個類型是如何實(shí)現(xiàn)的呢?
ReadonlyArray 數(shù)組類型
這個類型的實(shí)現(xiàn)還是很簡單的,就是只讀數(shù)組只有一個 at 方法,數(shù)組 at 方法的作用就是獲取一個整數(shù)值并返回該索引處的項(xiàng)目,允許參數(shù)是正整數(shù)和負(fù)整數(shù),負(fù)整數(shù)從數(shù)組的最后一項(xiàng)開始倒數(shù)。因此我們需要先實(shí)現(xiàn)這個只有 at 方法的接口,代碼如下:
interface RelativeIndexable<T> { at(index: number): T | undefined; }
而只讀數(shù)組類型 ReadonlyArray 只需要繼承這個接口就行了,代碼如下:
interface ReadonlyArray<T> extends RelativeIndexable<T> {}
實(shí)現(xiàn)比較兩個數(shù)組長度的類型
有了能夠獲取數(shù)組長度的類型,接下來比較兩個數(shù)組長度的類型就很簡單了,代碼如下:
type CompareLength< T extends ReadonlyArray<any>, U extends ReadonlyArray<any> > = Length<T> extends Length<U> ? true : false;
簡單來說,就是兩個數(shù)組長度一樣就返回 true,否則返回 false,這也限制了我們最終實(shí)現(xiàn)的類型 2 個參數(shù)的聯(lián)合類型最終提取出來的元素一定要一樣。
將屬性構(gòu)造成接口
接下來我們要實(shí)現(xiàn)將屬性構(gòu)造成接口,要想構(gòu)造成接口,那就需要屬性和屬性值,因此這個類型的實(shí)現(xiàn)是有 2 個參數(shù)的,可以看到我們最終實(shí)現(xiàn)的 Mapping 就是有 2 個參數(shù),第一個參數(shù)作為屬性,第二個參數(shù)作為屬性值。而由于接口屬性類型有限制,即只能是 PropertyKey 類型,因此我們是需要判斷的,同理,為了實(shí)現(xiàn)屬性和屬性值一一對應(yīng)有值的情況下,我們也需要對第二個參數(shù)做判斷,只有滿足 2 個參數(shù)類型都是 PropertyKey 類型,才能構(gòu)造成接口,并且構(gòu)造成接口我們可以使用 Record 類型。
根據(jù)以上分析,我們的最終代碼就實(shí)現(xiàn)如下:
// 不一定要叫Callback,也可以叫名字 type Callback<T, U> = T extends PropertyKey ? U extends PropertyKey ? Record<T, U> : never : never;
以上還涉及到了 ts 的兩個內(nèi)置類型,第一個是 PropertyKey 類型,第二個則是Record<T,U>
類型,下面我們來一一看下這 2 個類型的實(shí)現(xiàn)。
PropertyKey 類型
第一個 PropertyKey 類型非常簡單,它表示對象的屬性類型,我們只需要知道 js 對象的屬性只能是字符串或者數(shù)字或者符號就可以知道這個類型的實(shí)現(xiàn),代碼如下:
type PropertyKey = string | number | symbol;
可以看到這就是一個聯(lián)合類型,屬性的類型只能是字符串或者數(shù)值或者符號。
Record 類型
Record 類型表示構(gòu)造一個構(gòu)造一個具有類型 T 的一組屬性 U 的類型,我們只需要使用 in 操作符即可實(shí)現(xiàn),因?yàn)檫@個類型的第一個參數(shù)是要作為接口屬性的,而第二個參數(shù)則是作為對應(yīng)的屬性值。代碼如下:
type Record<T extends keyof any, U> = { [K in T]: U; };
這就是 Record 類型的實(shí)現(xiàn),這其中還設(shè)計(jì)到了 ts 的一個關(guān)鍵字,即 keyof,它表示提取類型的屬性,這個關(guān)鍵字通常用來提取接口的屬性,最后會返回組成屬性的聯(lián)合類型。例如:
type Test = { a: string; 1: number; }; type TestKey = keyof Test; // 'a' | 1
將兩個數(shù)組類型構(gòu)造成接口
有了前面的幾個類型的實(shí)現(xiàn),接下來我們需要實(shí)現(xiàn)一個根據(jù) 2 個參數(shù)數(shù)組構(gòu)造成接口的類型,為此我們需要定義第三個參數(shù),第三個參數(shù)應(yīng)該是一個接口對象,用來當(dāng)作最終返回的結(jié)果,默認(rèn)是一個空對象,而前面 2 個參數(shù)就是我們的屬性組成的只讀數(shù)組。結(jié)構(gòu)如下所示:
type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = any;
接下來第一步,首先我們需要比較 2 個參數(shù)數(shù)組長度應(yīng)該是一樣的,不一樣,我們就直接返回 never。如下所示:
type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = CompareLength<T, U> extends true ? any : never;
ps: 以上包括后面用 any 表示我們還沒有實(shí)現(xiàn),起一個占位符作用,方便我們理解實(shí)現(xiàn)思路。
緊接著第二步,我們需要判斷是否是空數(shù)組,只需要判斷其中一個即可,因?yàn)槲覀円呀?jīng)判斷了兩個數(shù)組長度是否相等,如果其中一個是空數(shù)組,那么另一個必定也是空數(shù)組,如果是空數(shù)組,直接返回結(jié)果即可,此時默認(rèn)值就是空對象,直接返回結(jié)果也合理,代碼如下:
type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = CompareLength<T, U> extends true ? (T extends [] ? R : any) : never;
接下來第三步,我們還需要做判斷,那就是如果 2 個數(shù)組都只有一個數(shù)組項(xiàng),那么我們只需要將第一個數(shù)組項(xiàng)提取出來,這里當(dāng)然是使用 infer 關(guān)鍵字來推導(dǎo)數(shù)組項(xiàng),然后使用 Callback 類型構(gòu)造成接口并與 R 結(jié)果取并集即可。代碼如下所示:
type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = CompareLength<T, U> extends true ? T extends [] ? R : T extends [infer F1] ? U extends [infer F2] ? R & Callback<F1, F2> : never : any : never;
第四步就是如果數(shù)組有多個數(shù)組項(xiàng),則我們需要遞歸的取并集。代碼如下所示:
type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = CompareLength<T, U> extends true ? T extends [] ? R : T extends [infer F1] ? U extends [infer F2] ? R & Callback<F1, F2> : never : T extends [infer F1, ...infer T1] ? U extends [infer F2, ...infer T2] ? Zip<T1, T2, R & Callback<F1, F2>> : never : never : never;
雖然這個類型的實(shí)現(xiàn)代碼比較長,但其實(shí)我們逐一拆分下來理解起來還是比較容易的。
實(shí)現(xiàn) Mapping 類型
有了前面幾個類型的實(shí)現(xiàn),最終我們就可以解答這道題了,我們只需要將 2 個聯(lián)合類型構(gòu)造成 2 個數(shù)組,然后使用 Zip 類型將 2 個類型組成的數(shù)組轉(zhuǎn)成接口即可,代碼如下:
type Mapping<T extends Union, Value extends string> = Zip< UnionToArray<T>, UnionToArray<Value> >;
以上代碼很好理解,我們將 2 個聯(lián)合類型使用 UnionToArray 構(gòu)造成 2 個類型數(shù)組,然后使用 Zip 類型構(gòu)造成接口。
優(yōu)化
不過以上代碼的實(shí)現(xiàn)還不算完美,因?yàn)槲覀冏罱K的結(jié)果是使用 Record 類型展示的,并不直觀,因此最后一步,我們還需要將 Record 類型轉(zhuǎn)成可以直觀看到的接口類型,很簡單,只需要讀取每一個接口屬性即可,和 Record 類型實(shí)現(xiàn)原理很類似。代碼如下:
type ToObj<T> = { [K in keyof T]: T[K]; };
最終實(shí)現(xiàn)版本
將優(yōu)化后的代碼與前面的實(shí)現(xiàn)合并,就得到了我們的最終實(shí)現(xiàn),代碼如下:
type Mapping<T extends Union, Value extends string> = ToObj< Zip<UnionToArray<T>, UnionToArray<Value>> >;
下面,我們將以上所有實(shí)現(xiàn)代碼整理到一起,代碼如下:
// 第一步 type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( k: infer I ) => void ? I : never; // 第二步 type UnionToOvlds<U> = UnionToIntersection< U extends any ? (f: U) => void : never >; // 第三步 type PopUnion<U> = UnionToOvlds<U> extends (f: infer A) => void ? A : never; // 第四步 type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true; // 第五步 type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]> : [T, ...A]; // 第六步 type Length<T extends ReadonlyArray<any>> = T extends { length: infer L } ? L : never; // 第七步 type CompareLength< T extends ReadonlyArray<any>, U extends ReadonlyArray<any> > = Length<T> extends Length<U> ? true : false; // 第八步 type Callback<T, U> = T extends PropertyKey ? U extends PropertyKey ? Record<T, U> : never : never; // 第九步 type Zip< T extends ReadonlyArray<any>, U extends ReadonlyArray<any>, R extends Record<string, any> = {} > = CompareLength<T, U> extends true ? T extends [] ? R : T extends [infer F1] ? U extends [infer F2] ? R & Callback<F1, F2> : never : T extends [infer F1, ...infer T1] ? U extends [infer F2, ...infer T2] ? Zip<T1, T2, R & Callback<F1, F2>> : never : never : never; // 第十步 type ToObj<T> = { [K in keyof T]: T[K]; }; // 最終 type Mapping<T extends Union, Value extends string> = ToObj< Zip<UnionToArray<T>, UnionToArray<Value>> >;
總結(jié)
下面我們來總結(jié)一下這道題中我們學(xué)到的知識點(diǎn):
- extends 關(guān)鍵字用于條件判斷。
- infer 關(guān)鍵字用于推導(dǎo)類型。
- keyof 關(guān)鍵字用于獲取對象接口屬性。
- ts 類型遞歸。
- ts 中的 3 個基本類型的含義,即 any,never,void 的含義。
- ts 中內(nèi)置類型的實(shí)現(xiàn),如: ReadonlyArray,Exclude,PropertyKey,Record。
以上的知識點(diǎn),在 ts 類型體操當(dāng)中將會經(jīng)常用到,所以需要理解深刻。
只是一道題目,我們就學(xué)到了 ts 的很多類型體操的知識,ts 類型這么有趣,難道不是嗎?
更多關(guān)于typescript類型體操的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TypeScript 基本數(shù)據(jù)類型實(shí)例詳解
這篇文章主要為大家介紹了TypeScript 基本數(shù)據(jù)類型實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01數(shù)據(jù)結(jié)構(gòu)TypeScript之鏈表實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了數(shù)據(jù)結(jié)構(gòu)TypeScript之鏈表實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01數(shù)據(jù)結(jié)構(gòu)TypeScript之棧和隊(duì)列詳解
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)TypeScript之棧和隊(duì)列詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析
這篇文章主要為大家介紹了TS中Array.reduce提示沒有與此調(diào)用匹配的重載解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06TypeScript 高級數(shù)據(jù)類型實(shí)例詳解
這篇文章主要為大家介紹了TypeScript 高級數(shù)據(jù)類型實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01移動設(shè)備web開發(fā)首選框架:zeptojs介紹
這篇文章主要介紹了移動設(shè)備web開發(fā)首選框架:zeptojs介紹,他兼容jquery的API,所以學(xué)起來或用起來并不吃力,需要的朋友可以參考下2015-01-01