Hello大家好,我是愣錘。隨著Typescript不可阻擋的趨勢(shì),相信小伙伴們或多或少的使用過(guò)Ts開(kāi)發(fā)了。而Ts的使用除了基本的類(lèi)型定義外,對(duì)于Ts的泛型、內(nèi)置高級(jí)類(lèi)型、自定義高級(jí)類(lèi)型工具等會(huì)相對(duì)陌生。本文將會(huì)通過(guò)22個(gè)類(lèi)型工具例子,深入講解Ts類(lèi)型工具原理和編程技巧。不扯閑篇,全程干貨,內(nèi)容非常多,想提升Ts功力的小伙伴請(qǐng)耐心讀下去。相信小伙伴們?cè)谧x完此文后,能夠?qū)@塊有更深入的理解。下面,我們開(kāi)始吧~
本文基本分為三部分:
第一部分講解一些基本的關(guān)鍵詞的特性(比如索引查詢、索引訪問(wèn)、映射、extends等),但是該部分更多的講解小伙伴們不清晰的一些特性,而基本功能則不再贅述。更多的關(guān)鍵詞及技巧將包含在后續(xù)的例子演示中再具體講述;
第二部分講解Ts內(nèi)置的類(lèi)型工具以及實(shí)現(xiàn)原理,比如Pick、Omit等;
第三部分講解自定義的工具類(lèi)型,該部分也是最難的部分,將通過(guò)一些復(fù)雜的類(lèi)型工具示例進(jìn)行逐步剖析,對(duì)于其中的晦澀的地方以及涉及的知識(shí)點(diǎn)逐步講解。此部分也會(huì)包含大量Ts類(lèi)型工具的編程技巧,也希望通過(guò)此部分的講解,小伙伴的Ts功底可以進(jìn)一步提升!
第一部分 前置內(nèi)容
keyof索引查詢
對(duì)應(yīng)任何類(lèi)型T,keyof T的結(jié)果為該類(lèi)型上所有共有屬性key的聯(lián)合:
interface Eg1 {
name: string,
readonly age: number,
}
// T1的類(lèi)型實(shí)則是name | age
type T1 = keyof Eg1
class Eg2 {
private name: string;
public readonly age: number;
protected home: string;
}
// T2實(shí)則被約束為 age
// 而name和home不是公有屬性,所以不能被keyof獲取到
type T2 = keyof Eg2
T[K]索引訪問(wèn)
interface Eg1 {
name: string,
readonly age: number,
}
// string
type V1 = Eg1['name']
// string | number
type V2 = Eg1['name' | 'age']
// any
type V2 = Eg1['name' | 'age2222']
// string | number
type V3 = Eg1[keyof Eg1]
T[keyof T]的方式,可以獲取到T所有key的類(lèi)型組成的聯(lián)合類(lèi)型;
T[keyof K]的方式,獲取到的是T中的key且同時(shí)存在于K時(shí)的類(lèi)型組成的聯(lián)合類(lèi)型;
注意:如果[]中的key有不存在T中的,則是any;因?yàn)閠s也不知道該key最終是什么類(lèi)型,所以是any;且也會(huì)報(bào)錯(cuò);
-
&交叉類(lèi)型注意點(diǎn)
交叉類(lèi)型取的多個(gè)類(lèi)型的并集,但是如果相同key但是類(lèi)型不同,則該key為never。
interface Eg1 {
name: string,
age: number,
}
interface Eg2 {
color: string,
age: string,
}
/**
* T的類(lèi)型為 {name: string; age: number; age: never}
* 注意,age因?yàn)镋g1和Eg2中的類(lèi)型不一致,所以交叉后age的類(lèi)型是never
*/
type T = Eg1 & Eg2
// 可通過(guò)如下示例驗(yàn)證
const val: T = {
name: '',
color: '',
age: (function a() {
throw Error()
})(),
}
extends關(guān)鍵詞特性(重點(diǎn))
- 用于接口,表示繼承
interface T1 {
name: string,
}
interface T2 {
sex: number,
}
/**
* @example
* T3 = {name: string, sex: number, age: number}
*/
interface T3 extends T1, T2 {
age: number,
}
注意,接口支持多重繼承,語(yǔ)法為逗號(hào)隔開(kāi)。如果是type實(shí)現(xiàn)繼承,則可以使用交叉類(lèi)型type A = B & C & D。
-
表示條件類(lèi)型,可用于條件判斷
表示條件判斷,如果前面的條件滿足,則返回問(wèn)號(hào)后的第一個(gè)參數(shù),否則第二個(gè)。類(lèi)似于js的三元運(yùn)算。
/**
* @example
* type A1 = 1
*/
type A1 = 'x' extends 'x' ? 1 : 2;
/**
* @example
* type A2 = 2
*/
type A2 = 'x' | 'y' extends 'x' ? 1 : 2;
/**
* @example
* type A3 = 1 | 2
*/
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'>
提問(wèn):為什么A2和A3的值不一樣?
- 如果用于簡(jiǎn)單的條件判斷,則是直接判斷前面的類(lèi)型是否可分配給后面的類(lèi)型
- 若extends前面的類(lèi)型是泛型,且泛型傳入的是聯(lián)合類(lèi)型時(shí),則會(huì)依次判斷該聯(lián)合類(lèi)型的所有子類(lèi)型是否可分配給extends后面的類(lèi)型(是一個(gè)分發(fā)的過(guò)程)。
總結(jié),就是extends前面的參數(shù)為聯(lián)合類(lèi)型時(shí)則會(huì)分解(依次遍歷所有的子類(lèi)型進(jìn)行條件判斷)聯(lián)合類(lèi)型進(jìn)行判斷。然后將最終的結(jié)果組成新的聯(lián)合類(lèi)型。
阻止extends關(guān)鍵詞對(duì)于聯(lián)合類(lèi)型的分發(fā)特性
如果不想被分解(分發(fā)),做法也很簡(jiǎn)單,可以通過(guò)簡(jiǎn)單的元組類(lèi)型包裹以下:
type P<T> = [T] extends ['x'] ? 1 : 2;
/**
* type A4 = 2;
*/
type A4 = P<'x' | 'y'>
類(lèi)型兼容性
集合論中,如果一個(gè)集合的所有元素在集合B中都存在,則A是B的子集;
類(lèi)型系統(tǒng)中,如果一個(gè)類(lèi)型的屬性更具體,則該類(lèi)型是子類(lèi)型。(因?yàn)閷傩愿賱t說(shuō)明該類(lèi)型約束的更寬泛,是父類(lèi)型)
因此,我們可以得出基本的結(jié)論:子類(lèi)型比父類(lèi)型更加具體,父類(lèi)型比子類(lèi)型更寬泛。 下面我們也將基于類(lèi)型的可復(fù)制性(可分配性)、協(xié)變、逆變、雙向協(xié)變等進(jìn)行進(jìn)一步的講解。
- 可賦值性
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
let a: Animal;
let b: Dog;
// 可以賦值,子類(lèi)型更佳具體,可以賦值給更佳寬泛的父類(lèi)型
a = b;
// 反過(guò)來(lái)不行
b = a;
- 可賦值性在聯(lián)合類(lèi)型中的特性
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;
// 不可賦值
b = a;
// 可以賦值
a = b;
是不是A的類(lèi)型更多,A就是子類(lèi)型呢?恰恰相反,A此處類(lèi)型更多但是其表達(dá)的類(lèi)型更寬泛,所以A是父類(lèi)型,B是子類(lèi)型。
因此b = a不成立(父類(lèi)型不能賦值給子類(lèi)型),而a = b成立(子類(lèi)型可以賦值給父類(lèi)型)
- 協(xié)變
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
let Eg1: Animal;
let Eg2: Dog;
// 兼容,可以賦值
Eg1 = Eg2;
let Eg3: Array<Animal>
let Eg4: Array<Dog>
// 兼容,可以賦值
Eg3 = Eg4
通過(guò)Eg3和Eg4來(lái)看,在Animal和Dog在變成數(shù)組后,Array<Dog>依舊可以賦值給Array<Animal>,因此對(duì)于type MakeArray = Array<any>來(lái)說(shuō)就是協(xié)變的。
最后引用維基百科中的定義:
協(xié)變與逆變(Covariance and contravariance )是在計(jì)算機(jī)科學(xué)中,描述具有父/子型別關(guān)系的多個(gè)型別通過(guò)型別構(gòu)造器、構(gòu)造出的多個(gè)復(fù)雜型別之間是否有父/子型別關(guān)系的用語(yǔ)。
簡(jiǎn)單說(shuō)就是,具有父子關(guān)系的多個(gè)類(lèi)型,在通過(guò)某種構(gòu)造關(guān)系構(gòu)造成的新的類(lèi)型,如果還具有父子關(guān)系則是協(xié)變的,而關(guān)系逆轉(zhuǎn)了(子變父,父變子)就是逆變的??赡苈?tīng)起來(lái)有些抽象,下面我們將用更具體的例子進(jìn)行演示說(shuō)明:
- 逆變
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
type AnimalFn = (arg: Animal) => void
type DogFn = (arg: Dog) => void
let Eg1: AnimalFn;
let Eg2: DogFn;
// 不再可以賦值了,
// AnimalFn = DogFn不可以賦值了, Animal = Dog是可以的
Eg1 = Eg2;
// 反過(guò)來(lái)可以
Eg2 = Eg1;
理論上,Animal = Dog是類(lèi)型安全的,那么AnimalFn = DogFn也應(yīng)該類(lèi)型安全才對(duì),為什么Ts認(rèn)為不安全呢?看下面的例子:
let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => {
arg.break();
}
// 假設(shè)類(lèi)型安全可以賦值
animal = dog;
// 那么animal在調(diào)用時(shí)約束的參數(shù),缺少dog所需的參數(shù),此時(shí)會(huì)導(dǎo)致錯(cuò)誤
animal({name: 'cat'});
從這個(gè)例子看到,如果dog函數(shù)賦值給animal函數(shù),那么animal函數(shù)在調(diào)用時(shí),約束的是參數(shù)必須要為Animal類(lèi)型(而不是Dog),但是animal實(shí)際為dog的調(diào)用,此時(shí)就會(huì)出現(xiàn)錯(cuò)誤。
因此,Animal和Dog在進(jìn)行type Fn<T> = (arg: T) => void構(gòu)造器構(gòu)造后,父子關(guān)系逆轉(zhuǎn)了,此時(shí)成為“逆變”。
- 雙向協(xié)變
Ts在函數(shù)參數(shù)的比較中實(shí)際上默認(rèn)采取的策略是雙向協(xié)變:只有當(dāng)源函數(shù)參數(shù)能夠賦值給目標(biāo)函數(shù)或者反過(guò)來(lái)時(shí)才能賦值成功。
這是不穩(wěn)定的,因?yàn)檎{(diào)用者可能傳入了一個(gè)具有更精確類(lèi)型信息的函數(shù),但是調(diào)用這個(gè)傳入的函數(shù)的時(shí)候卻使用了不是那么精確的類(lèi)型信息(典型的就是上述的逆變)。 但是實(shí)際上,這極少會(huì)發(fā)生錯(cuò)誤,并且能夠?qū)崿F(xiàn)很多JavaScript里的常見(jiàn)模式:
// lib.dom.d.ts中EventListener的接口定義
interface EventListener {
(evt: Event): void;
}
// 簡(jiǎn)化后的Event
interface Event {
readonly target: EventTarget | null;
preventDefault(): void;
}
// 簡(jiǎn)化合并后的MouseEvent
interface MouseEvent extends Event {
readonly x: number;
readonly y: number;
}
// 簡(jiǎn)化后的Window接口
interface Window {
// 簡(jiǎn)化后的addEventListener
addEventListener(type: string, listener: EventListener)
}
// 日常使用
window.addEventListener('click', (e: Event) => {});
window.addEventListener('mouseover', (e: MouseEvent) => {});
可以看到Window的listener函數(shù)要求參數(shù)是Event,但是日常使用時(shí)更多時(shí)候傳入的是Event子類(lèi)型。但是這里可以正常使用,正是其默認(rèn)行為是雙向協(xié)變的原因。可以通過(guò)tsconfig.js中修改strictFunctionType屬性來(lái)嚴(yán)格控制協(xié)變和逆變。
敲重點(diǎn)!??!敲重點(diǎn)?。?!敲重點(diǎn)!?。?/strong>
infer關(guān)鍵詞的功能暫時(shí)先不做太詳細(xì)的說(shuō)明了,主要是用于extends的條件類(lèi)型中讓Ts自己推到類(lèi)型,具體的可以查閱官網(wǎng)。但是關(guān)于infer的一些容易讓人忽略但是非常重要的特性,這里必須要提及一下:
-
infer推導(dǎo)的名稱相同并且都處于逆變的位置,則推導(dǎo)的結(jié)果將會(huì)是交叉類(lèi)型。
type Bar<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
} ? U : never;
// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
- infer推導(dǎo)的名稱相同并且都處于協(xié)變的位置,則推導(dǎo)的結(jié)果將會(huì)是聯(lián)合類(lèi)型。
type Foo<T> = T extends {
a: infer U;
b: infer U;
} ? U : never;
// type T1 = string
type T1 = Foo<{ a: string; b: string }>;
// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;
inter與協(xié)變逆變的參考文檔點(diǎn)擊這里
第二部分 Ts內(nèi)置類(lèi)型工具原理解析
Partial實(shí)現(xiàn)原理解析
Partial<T>將T的所有屬性變成可選的。
/**
* 核心實(shí)現(xiàn)就是通過(guò)映射類(lèi)型遍歷T上所有的屬性,
* 然后將每個(gè)屬性設(shè)置為可選屬性
*/
type Partial<T> = {
[P in keyof T]?: T[P];
}
-
[P in keyof T]通過(guò)映射類(lèi)型,遍歷T上的所有屬性 -
?:設(shè)置為屬性為可選的 -
T[P]設(shè)置類(lèi)型為原來(lái)的類(lèi)型
擴(kuò)展一下,將制定的key變成可選類(lèi)型:
/**
* 主要通過(guò)K extends keyof T約束K必須為keyof T的子類(lèi)型
* keyof T得到的是T的所有key組成的聯(lián)合類(lèi)型
*/
type PartialOptional<T, K extends keyof T> = {
[P in K]?: T[P];
}
/**
* @example
* type Eg1 = { key1?: string; key2?: number }
*/
type Eg1 = PartialOptional<{
key1: string,
key2: number,
key3: ''
}, 'key1' | 'key2'>;
Readonly原理解析
/**
* 主要實(shí)現(xiàn)是通過(guò)映射遍歷所有key,
* 然后給每個(gè)key增加一個(gè)readonly修飾符
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
/**
* @example
* type Eg = {
* readonly key1: string;
* readonly key2: number;
* }
*/
type Eg = Readonly<{
key1: string,
key2: number,
}>
Pick
挑選一組屬性并組成一個(gè)新的類(lèi)型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
基本和上述同樣的知識(shí)點(diǎn),就不再贅述了。
Record
構(gòu)造一個(gè)type,key為聯(lián)合類(lèi)型中的每個(gè)子類(lèi)型,類(lèi)型為T。文字不好理解,先看例子:
/**
* @example
* type Eg1 = {
* a: { key1: string; };
* b: { key1: string; };
* }
* @desc 就是遍歷第一個(gè)參數(shù)'a' | 'b'的每個(gè)子類(lèi)型,然后將值設(shè)置為第二參數(shù)
*/
type Eg1 = Record<'a' | 'b', {key1: string}>
Record具體實(shí)現(xiàn):
/**
* 核心實(shí)現(xiàn)就是遍歷K,將值設(shè)置為T(mén)
*/
type Record<K extends keyof any, T> = {
[P in K]: T
}
/**
* @example
* type Eg2 = {a: B, b: B}
*/
interface A {
a: string,
b: number,
}
interface B {
key1: number,
key2: string,
}
type Eg2 = Record<keyof A, B>
- 值得注意的是keyof any得到的是
string | number | symbol - 原因在于類(lèi)型key的類(lèi)型只能為
string | number | symbol
擴(kuò)展: 同態(tài)與非同態(tài)。劃重點(diǎn)?。?! 劃重點(diǎn)?。?! 劃重點(diǎn)?。?!
- Partial、Readonly和Pick都屬于同態(tài)的,即其實(shí)現(xiàn)需要輸入類(lèi)型T來(lái)拷貝屬性,因此屬性修飾符(例如readonly、?:)都會(huì)被拷貝。可從下面例子驗(yàn)證:
/**
* @example
* type Eg = {readonly a?: string}
*/
type Eg = Pick<{readonly a?: string}, 'a'>
從Eg的結(jié)果可以看到,Pick在拷貝屬性時(shí),連帶拷貝了readonly和?:的修飾符。
- Record是非同態(tài)的,不需要拷貝屬性,因此不會(huì)拷貝屬性修飾符
可以看到Pick的實(shí)現(xiàn)中,注意P in K(本質(zhì)是P in keyof T),T為輸入的類(lèi)型,而keyof T則遍歷了輸入類(lèi)型;而Record的實(shí)現(xiàn)中,并沒(méi)有遍歷所有輸入的類(lèi)型,K只是約束為keyof any的子類(lèi)型即可。
最后再類(lèi)比一下Pick、Partial、readonly這幾個(gè)類(lèi)型工具,無(wú)一例外,都是使用到了keyof T來(lái)輔助拷貝傳入類(lèi)型的屬性。
Exclude原理解析
Exclude<T, U>提取存在于T,但不存在于U的類(lèi)型組成的聯(lián)合類(lèi)型。
/**
* 遍歷T中的所有子類(lèi)型,如果該子類(lèi)型約束于U(存在于U、兼容于U),
* 則返回never類(lèi)型,否則返回該子類(lèi)型
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* @example
* type Eg = 'key1'
*/
type Eg = Exclude<'key1' | 'key2', 'key2'>
敲重點(diǎn)?。?!
-
never表示一個(gè)不存在的類(lèi)型 -
never與其他類(lèi)型的聯(lián)合后,是沒(méi)有never的
/**
* @example
* type Eg2 = string | number
*/
type Eg2 = string | number | never
因此上述Eg其實(shí)就等于key1 | never,也就是type Eg = key1
Extract
Extract<T, U>提取聯(lián)合類(lèi)型T和聯(lián)合類(lèi)型U的所有交集。
type Extract<T, U> = T extends U ? T : never;
/**
* @example
* type Eg = 'key1'
*/
type Eg = Extract<'key1' | 'key2', 'key1'>
Omit原理解析
Omit<T, K>從類(lèi)型T中剔除K中的所有屬性。
/**
* 利用Pick實(shí)現(xiàn)Omit
*/
type Omit = Pick<T, Exclude<keyof T, K>>;
- 換種思路想一下,其實(shí)現(xiàn)可以是利用
Pick提取我們需要的keys組成的類(lèi)型 - 因此也就是
Omit = Pick<T, 我們需要的屬性聯(lián)合> - 而我們需要的屬性聯(lián)合就是,從
T的屬性聯(lián)合中排出存在于聯(lián)合類(lèi)型K中的 - 因此也就是
Exclude<keyof T, K>;
如果不利用Pick實(shí)現(xiàn)呢?
/**
* 利用映射類(lèi)型Omit
*/
type Omit2<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P]
}
- 其實(shí)現(xiàn)類(lèi)似于
Pick的原理實(shí)現(xiàn) - 區(qū)別在于是遍歷的我們需要的屬性不一樣
- 我們需要的屬性和上面的例子一樣,就是
Exclude<keyof T, K> - 因此,遍歷就是
[P in Exclude<keyof T, K>]
Parameters 和 ReturnType
Parameters 獲取函數(shù)的參數(shù)類(lèi)型,將每個(gè)參數(shù)類(lèi)型放在一個(gè)元組中。
/**
* @desc 具體實(shí)現(xiàn)
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
/**
* @example
* type Eg = [arg1: string, arg2: number];
*/
type Eg = Parameters<(arg1: string, arg2: number) => void>;
-
Parameters首先約束參數(shù)T必須是個(gè)函數(shù)類(lèi)型,所以(...args: any) => any>替換成Function也是可以的 - 具體實(shí)現(xiàn)就是,判斷T是否是函數(shù)類(lèi)型,如果是則使用
inter P讓ts自己推導(dǎo)出函數(shù)的參數(shù)類(lèi)型,并將推導(dǎo)的結(jié)果存到類(lèi)型P上,否則就返回never;
敲重點(diǎn)?。?!敲重點(diǎn)!?。∏弥攸c(diǎn)?。?!
-
infer關(guān)鍵詞作用是讓Ts自己推導(dǎo)類(lèi)型,并將推導(dǎo)結(jié)果存儲(chǔ)在其參數(shù)綁定的類(lèi)型上。Eg:infer P就是將結(jié)果存在類(lèi)型P上,供使用。 -
infer關(guān)鍵詞只能在extends條件類(lèi)型上使用,不能在其他地方使用。
再敲重點(diǎn)?。?!再敲重點(diǎn)?。?!再敲重點(diǎn)!?。?/strong>
type Eg = [arg1: string, arg2: number]這是一個(gè)元組,但是和我們常見(jiàn)的元組type tuple = [string, number]。官網(wǎng)未提到該部分文檔說(shuō)明,其實(shí)可以把這個(gè)作為類(lèi)似命名元組,或者具名元組的意思去理解。實(shí)質(zhì)上沒(méi)有什么特殊的作用,比如無(wú)法通過(guò)這個(gè)具名去取值不行的。但是從語(yǔ)義化的角度,個(gè)人覺(jué)得多了語(yǔ)義化的表達(dá)罷了。定義元祖的可選項(xiàng),只能是最后的選項(xiàng)
/**
* 普通方式
*/
type Tuple1 = [string, number?];
const a: Tuple1 = ['aa', 11];
const a2: Tuple1 = ['aa'];
/**
* 具名方式
*/
type Tuple2 = [name: string, age?: number];
const b: Tuple2 = ['aa', 11];
const b2: Tuple2 = ['aa'];
擴(kuò)展:infer實(shí)現(xiàn)一個(gè)推導(dǎo)數(shù)組所有元素的類(lèi)型:
/**
* 約束參數(shù)T為數(shù)組類(lèi)型,
* 判斷T是否為數(shù)組,如果是數(shù)組類(lèi)型則推導(dǎo)數(shù)組元素的類(lèi)型
*/
type FalttenArray<T extends Array<any>> = T extends Array<infer P> ? P : never;
/**
* type Eg1 = number | string;
*/
type Eg1 = FalttenArray<[number, string]>
/**
* type Eg2 = 1 | 'asd';
*/
type Eg2 = FalttenArray<[1, 'asd']>
ReturnType 獲取函數(shù)的返回值類(lèi)型。
/**
* @desc ReturnType的實(shí)現(xiàn)其實(shí)和Parameters的基本一樣
* 無(wú)非是使用infer R的位置不一樣。
*/
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
ConstructorParameters
ConstructorParameters可以獲取類(lèi)的構(gòu)造函數(shù)的參數(shù)類(lèi)型,存在一個(gè)元組中。
/**
* 核心實(shí)現(xiàn)還是利用infer進(jìn)行推導(dǎo)構(gòu)造函數(shù)的參數(shù)類(lèi)型
*/
type ConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
/**
* @example
* type Eg = string;
*/
interface ErrorConstructor {
new(message?: string): Error;
(message?: string): Error;
readonly prototype: Error;
}
type Eg = ConstructorParameters<ErrorConstructor>;
/**
* @example
* type Eg2 = [name: string, sex?: number];
*/
class People {
constructor(public name: string, sex?: number) {}
}
type Eg2 = ConstructorParameters<typeof People>
- 首先約束參數(shù)T為擁有構(gòu)造函數(shù)的類(lèi)。注意這里有個(gè)
abstract修飾符,等下會(huì)說(shuō)明。 - 實(shí)現(xiàn)時(shí),判斷T是滿足約束的類(lèi)時(shí),利用
infer P自動(dòng)推導(dǎo)構(gòu)造函數(shù)的參數(shù)類(lèi)型,并最終返回該類(lèi)型。
敲重點(diǎn)?。?!敲重點(diǎn)?。。∏弥攸c(diǎn)?。?!
那么疑問(wèn)來(lái)了,為什么要對(duì)T要約束為abstract抽象類(lèi)呢?看下面例子:
/**
* 定義一個(gè)普通類(lèi)
*/
class MyClass {}
/**
* 定義一個(gè)抽象類(lèi)
*/
abstract class MyAbstractClass {}
// 可以賦值
const c1: typeof MyClass = MyClass
// 報(bào)錯(cuò),無(wú)法將抽象構(gòu)造函數(shù)類(lèi)型分配給非抽象構(gòu)造函數(shù)類(lèi)型
const c2: typeof MyClass = MyAbstractClass
// 可以賦值
const c3: typeof MyAbstractClass = MyClass
// 可以賦值
const c4: typeof MyAbstractClass = MyAbstractClass
由此看出,如果將類(lèi)型定義為抽象類(lèi)(抽象構(gòu)造函數(shù)),則既可以賦值為抽象類(lèi),也可以賦值為普通類(lèi);而反之則不行。
再敲重點(diǎn)?。?!再敲重點(diǎn)?。?!再敲重點(diǎn)?。?!
這里繼續(xù)提問(wèn),直接使用類(lèi)作為類(lèi)型,和使用typeof 類(lèi)作為類(lèi)型,有什么區(qū)別呢?
/**
* 定義一個(gè)類(lèi)
*/
class People {
name: number;
age: number;
constructor() {}
}
// p1可以正常賦值
const p1: People = new People();
// 等號(hào)后面的People報(bào)錯(cuò),類(lèi)型“typeof People”缺少類(lèi)型“People”中的以下屬性: name, age
const p2: People = People;
// p3報(bào)錯(cuò),類(lèi)型 "People" 中缺少屬性 "prototype",但類(lèi)型 "typeof People" 中需要該屬性
const p3: typeof People = new People();
// p4可以正常賦值
const p4: typeof People = People;
結(jié)論是這樣的:
- 當(dāng)把類(lèi)直接作為類(lèi)型時(shí),該類(lèi)型約束的是該類(lèi)型必須是類(lèi)的實(shí)例;即該類(lèi)型獲取的是該類(lèi)上的實(shí)例屬性和實(shí)例方法(也叫原型方法);
- 當(dāng)把typeof 類(lèi)作為類(lèi)型時(shí),約束的滿足該類(lèi)的類(lèi)型;即該類(lèi)型獲取的是該類(lèi)上的靜態(tài)屬性和方法。
最后,只需要對(duì)infer的使用換個(gè)位置,便可以獲取構(gòu)造函數(shù)返回值的類(lèi)型:
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
Ts compiler內(nèi)部實(shí)現(xiàn)的類(lèi)型
Uppercase
/**
* @desc 構(gòu)造一個(gè)將字符串轉(zhuǎn)大寫(xiě)的類(lèi)型
* @example
* type Eg1 = 'ABCD';
*/
type Eg1 = Uppercase<'abcd'>;
Lowercase
/**
* @desc 構(gòu)造一個(gè)將字符串轉(zhuǎn)小大寫(xiě)的類(lèi)型
* @example
* type Eg2 = 'abcd';
*/
type Eg2 = Lowercase<'ABCD'>;
Capitalize
/**
* @desc 構(gòu)造一個(gè)將字符串首字符轉(zhuǎn)大寫(xiě)的類(lèi)型
* @example
* type Eg3 = 'Abcd';
*/
type Eg3 = Capitalize<'abcd'>;
Uncapitalize
/**
* @desc 構(gòu)造一個(gè)將字符串首字符轉(zhuǎn)小寫(xiě)的類(lèi)型
* @example
* type Eg3 = 'aBCD';
*/
type Eg3 = Uncapitalize<'ABCD'>;
這些類(lèi)型工具,在lib.es5.d.ts文件中是看不到具體定義的:
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
第三部分 自定義Ts高級(jí)類(lèi)型工具及類(lèi)型編程技巧
SymmetricDifference
SymmetricDifference<T, U>獲取沒(méi)有同時(shí)存在于T和U內(nèi)的類(lèi)型。
/**
* 核心實(shí)現(xiàn)
*/
type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;
/**
* SetDifference的實(shí)現(xiàn)和Exclude一樣
*/
type SymmetricDifference<T, U> = Exclude<T | U, T & U>;
/**
* @example
* type Eg = '1' | '4';
*/
type Eg = SymmetricDifference<'1' | '2' | '3', '2' | '3' | '4'>
其核心實(shí)現(xiàn)利用了3點(diǎn):分發(fā)式聯(lián)合類(lèi)型、交叉類(lèi)型和Exclude。
- 首先利用
Exclude從獲取存在于第一個(gè)參數(shù)但是不存在于第二個(gè)參數(shù)的類(lèi)型 -
Exclude第2個(gè)參數(shù)是T & U獲取的是所有類(lèi)型的交叉類(lèi)型 -
Exclude第一個(gè)參數(shù)則是T | U,這是利用在聯(lián)合類(lèi)型在extends中的分發(fā)特性,可以理解為Exclude<T, T & U> | Exclude<U, T & U>;
總結(jié)一下就是,提取存在于T但不存在于T & U的類(lèi)型,然后再提取存在于U但不存在于T & U的,最后進(jìn)行聯(lián)合。
FunctionKeys
獲取T中所有類(lèi)型為函數(shù)的key組成的聯(lián)合類(lèi)型。
/**
* @desc NonUndefined判斷T是否為undefined
*/
type NonUndefined<T> = T extends undefined ? never : T;
/**
* @desc 核心實(shí)現(xiàn)
*/
type FunctionKeys<T extends object> = {
[K in keyof T]: NonUndefined<T[K]> extends Function ? K : never;
}[keyof T];
/**
* @example
* type Eg = 'key2' | 'key3';
*/
type AType = {
key1: string,
key2: () => void,
key3: Function,
};
type Eg = FunctionKeys<AType>;
- 首先約束參數(shù)
T類(lèi)型為object - 通過(guò)映射類(lèi)型
K in keyof T遍歷所有的key,先通過(guò)NonUndefined<T[K]>過(guò)濾T[K]為undefined | null的類(lèi)型,不符合的返回never - 若
T[K]為有效類(lèi)型,則判斷是否為Function類(lèi)型,是的話返回K,否則never;此時(shí)可以得到的類(lèi)型,例如:
/**
* 上述的Eg在此時(shí)應(yīng)該是如下類(lèi)型,偽代碼:
*/
type TempType = {
key1: never,
key2: 'key2',
key3: 'key3',
}
最后經(jīng)過(guò){省略}[keyof T]索引訪問(wèn),取到的為值類(lèi)型的聯(lián)合類(lèi)型never | key2 | key3,計(jì)算后就是key2 | key3;
敲重點(diǎn)?。?!敲重點(diǎn)?。。∏弥攸c(diǎn)?。?!
-
T[]是索引訪問(wèn)操作,可以取到值的類(lèi)型 -
T['a' | 'b']若[]內(nèi)參數(shù)是聯(lián)合類(lèi)型,則也是分發(fā)索引的特性,依次取到值的類(lèi)型進(jìn)行聯(lián)合 -
T[keyof T]則是獲取T所有值的類(lèi)型類(lèi)型; -
never和其他類(lèi)型進(jìn)行聯(lián)合時(shí),never是不存在的。例如:never | number | string等同于number | string
再敲重點(diǎn)!?。≡偾弥攸c(diǎn)?。?!再敲重點(diǎn)!??!
-
null和undefined可以賦值給其他類(lèi)型(開(kāi)始該類(lèi)型的嚴(yán)格賦值檢測(cè)除外),所以上述實(shí)現(xiàn)中需要使用`NonUndefined先行判斷。 - NonUndefined中的實(shí)現(xiàn),只判斷了
T extends undefined,其實(shí)也是因?yàn)閮烧呖梢曰ハ嗉嫒莸?。所以你換成T extends null或者T extends null | undefined都是可以的。
// A = 1
type A = undefined extends null ? 1 : 2;
// B = 1
type B = null extends undefined ? 1 : 2;
最后,如果你想寫(xiě)一個(gè)獲取非函數(shù)類(lèi)型的key組成的聯(lián)合類(lèi)型,無(wú)非就是K和never的位置不一樣罷了。同樣,你也可以實(shí)現(xiàn)StringKeys、NumberKeys等等。但是記得可以抽象個(gè)工廠類(lèi)型哈:
type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
/**
* @desc 用于創(chuàng)建獲取指定類(lèi)型工具的類(lèi)型工廠
* @param T 待提取的類(lèi)型
* @param P 要?jiǎng)?chuàng)建的類(lèi)型
* @param IsCheckNon 是否要進(jìn)行null和undefined檢查
*/
type KeysFactory<T, P extends Primitive | Function | object, IsCheckNon extends boolean> = {
[K in keyof T]: IsCheckNon extends true
? (NonUndefined<T[K]> extends P ? K : never)
: (T[K] extends P ? K : never);
}[keyof T];
/**
* @example
* 例如上述KeysFactory就可以通過(guò)工廠類(lèi)型進(jìn)行創(chuàng)建了
*/
type FunctionKeys<T> = KeysFactory<T, Function, true>;
type StringKeys<T> = KeysFactory<T, string, true>;
type NumberKeys<T> = KeysFactory<T, string, true>;
MutableKeys
MutableKeys<T>查找T所有非只讀類(lèi)型的key組成的聯(lián)合類(lèi)型。
/**
* 核心實(shí)現(xiàn)
*/
type MutableKeys<T extends object> = {
[P in keyof T]-?: IfEquals<
{ [Q in P]: T[P] },
{ -readonly [Q in P]: T[P] },
P
>;
}[keyof T];
/**
* @desc 一個(gè)輔助類(lèi)型,判斷X和Y是否類(lèi)型相同,
* @returns 是則返回A,否則返回B
*/
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
? A
: B;
MutableKeys還是有一定難度的,講解MutableKeys的實(shí)現(xiàn),我們要分下面幾個(gè)步驟:
第一步,先理解只讀和非只讀的一些特性
/**
* 遍歷類(lèi)型T,原封不動(dòng)的返回,有點(diǎn)類(lèi)似于拷貝類(lèi)型的意思
*/
type RType1<T> = {
[P in keyof T]: T[P];
}
/**
* 遍歷類(lèi)型T,將每個(gè)key變成非只讀
* 或者理解成去掉只讀屬性更好理解。
*/
type RType2<T> = {
-readonly[P in keyof T]: T[P];
}
// R0 = { a: string; readonly b: number }
type R0 = RType1<{a: string, readonly b: number}>
// R1 = { a: string }
type R1 = RType1<{a: string}>;
// R2 = { a: string }
type R2 = RType2<{a: string}>;
// R3 = { readonly a: string }
type R3 = RType1<{readonly a: string}>;
// R4 = { a: string }
type R4 = RType2<{readonly a: string}>;
可以看到:RType1和RType2的參數(shù)為非只讀的屬性時(shí),R1和R2的結(jié)果是一樣的;RType1和RType2的參數(shù)為只讀的屬性時(shí),得到的結(jié)果R3是只讀的,R4是非只讀的。所以,這里要敲個(gè)重點(diǎn)了:
-
[P in Keyof T]是映射類(lèi)型,而映射是同態(tài)的,同態(tài)即會(huì)拷貝原有的屬性修飾符等??梢詤⒖糝0的例子。 - 映射類(lèi)型上的
-readonly表示為非只讀,或者可以理解為去掉只讀。對(duì)于只讀屬性加上-readonly變成了非只讀,而對(duì)非只讀屬性加上-readonly后還是非只讀。一種常見(jiàn)的使用方式,比如你想把屬性變成都是非只讀的,不能前面不加修飾符(雖然不寫(xiě)就表示非只讀),但是要考慮到同態(tài)拷貝的問(wèn)題。
第二步,解析IfEquals
IfEquals用于判斷類(lèi)型X和Y是否相同,相等則返回A,否則返回B。這個(gè)函數(shù)是比較難的,也別怕啦,下面講完就妥妥的明白啦~
type IfEquals<X, Y, A = X, B = never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2)
? A : B;
- 首先
IfEquals<X, Y, A, B>的四個(gè)參數(shù),X和Y是待比較的兩個(gè)類(lèi)型,如果相等則返回A,不相等返回B。 -
IfEquals的基本骨架是type IfEquals<> = (參數(shù)1) extends (參數(shù)2) ? A : B這樣的,就是判斷如果參數(shù)1的類(lèi)型能夠分配給參數(shù)2的類(lèi)型,則返回A,否則返回B;
參數(shù)1和參數(shù)2的基本結(jié)構(gòu)是一樣的,唯一區(qū)別在于X和Y不同。這里看下具體下面的例子:
// A = <T>() => T extends string ? 1 : 2;
type A = <T>() => T extends string ? 1 : 2;
// B = <T>() => T extends number ? 1 : 2;
type B = <T>() => T extends number ? 1 : 2;
// C = 2
type C = A extends B ? 1 : 2;
第3步,解析MutableKeys實(shí)現(xiàn)邏輯
-
MutableKeys首先約束T為object類(lèi)型 - 通過(guò)映射類(lèi)型
[P in keyof T]進(jìn)行遍歷,key對(duì)應(yīng)的值則是IfEquals<類(lèi)型1, 類(lèi)型2, P>,如果類(lèi)型1和類(lèi)型2相等則返回對(duì)應(yīng)的P(也就是key),否則返回never。
而P其實(shí)就是一個(gè)只有一個(gè)當(dāng)前key的聯(lián)合類(lèi)型,所以[Q in P]: T[P]也只是一個(gè)普通的映射類(lèi)型。但是要注意的是參數(shù)1{ [Q in P]: T[P] }是通過(guò){}構(gòu)造的一個(gè)類(lèi)型,參數(shù)2{ -readonly [Q in P]: T[P] }也是通過(guò){}構(gòu)造的一個(gè)類(lèi)型,兩者的唯一區(qū)別即使-readonly。
- 所以這里就有意思了,回想一下上面的第一步的例子,是不是就理解了:如果P是只讀的,那么參數(shù)1和參數(shù)2的P最終都是只讀的;如果P是非只讀的,則參數(shù)1的P為非只讀的,而參數(shù)2的P被-readonly去掉了非只讀屬性從而變成了只讀屬性。因此就完成了篩選:P為非只讀時(shí)IfEquals返回的P,P為只讀時(shí)IfEquals返回never。
所以key為非只讀時(shí),類(lèi)型為key,否則類(lèi)型為never,最后通過(guò)[keyof T]得到了所有非只讀key的聯(lián)合類(lèi)型。
OptionalKeys
OptionalKeys<T>提取T中所有可選類(lèi)型的key組成的聯(lián)合類(lèi)型。
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T];
type Eg = OptionalKeys<{key1?: string, key2: number}>
核心實(shí)現(xiàn),用映射類(lèi)型遍歷所有key,通過(guò)Pick<T, P>提取當(dāng)前key和類(lèi)型。注意,這里也是利用了同態(tài)拷貝會(huì)拷貝可選修飾符的特性。
利用{} extends {當(dāng)前key: 類(lèi)型}判斷是否是可選類(lèi)型。
// Eg2 = false
type Eg2 = {} extends {key1: string} ? true : false;
// Eg3 = true
type Eg3 = {} extends {key1?: string} ? true : false;
利用的就是{}和只包含可選參數(shù)類(lèi)型{key?: string}是兼容的這一特性。把extends前面的{}替換成object也是可以的。
增強(qiáng)Pick
PickByValue提取指定值的類(lèi)型
// 輔助函數(shù),用于獲取T中類(lèi)型不為never的類(lèi)型組成的聯(lián)合類(lèi)型
type TypeKeys<T> = T[keyof T];
/**
* 核心實(shí)現(xiàn)
*/
type PickByValue<T, V> = Pick<T,
TypeKeys<{[P in keyof T]: T[P] extends V ? P : never}>
>;
/**
* @example
* type Eg = {
* key1: number;
* key3: number;
* }
*/
type Eg = PickByValue<{key1: number, key2: string, key3: number}, number>;
Ts的類(lèi)型兼容特性,所以類(lèi)似string是可以分配給string | number的,因此上述并不是精準(zhǔn)的提取方式。如果實(shí)現(xiàn)精準(zhǔn)的方式,則可以考慮下面?zhèn)€這個(gè)類(lèi)型工具。
PickByValueExact精準(zhǔn)的提取指定值的類(lèi)型
/**
* 核心實(shí)現(xiàn)
*/
type PickByValueExact<T, V> = Pick<T,
TypeKeys<{[P in keyof T]: [T[P]] extends [V]
? ([V] extends [T[P]] ? P : never)
: never;
}>
>
// type Eg1 = { b: number };
type Eg1 = PickByValueExact<{a: string, b: number}, number>
// type Eg2 = { b: number; c: number | undefined }
type Eg2 = PickByValueExact<{a: string, b: number, c: number | undefined}, number>
PickByValueExact的核心實(shí)現(xiàn)主要有三點(diǎn):
一是利用Pick提取我們需要的key對(duì)應(yīng)的類(lèi)型
二是利用給泛型套一層元組規(guī)避extends的分發(fā)式聯(lián)合類(lèi)型的特性
三是利用兩個(gè)類(lèi)型互相兼容的方式判斷是否相同。
具體可以看下下面例子:
type Eq1<X, Y> = X extends Y ? true : false;
type Eq2<X, Y> = [X] extends [Y] ? true : false;
type Eq3<X, Y> = [X] extends [Y]
? ([Y] extends [X] ? true : false)
: false;
// boolean, 期望是false
type Eg1 = Eq1<string | number, string>
// false
type Eg2 = Eq2<string | number, string>
// true,期望是false
type Eg3 = Eq2<string, string | number>
// false
type Eg4 = Eq3<string, string | number>
// true,非strictNullChecks模式下的結(jié)果
type Eg5 = Eq3<number | undefined, number>
// false,strictNullChecks模式下的結(jié)果
type Eg6 = Eq3<number | undefined, number>
- 從Eg1和Eg2對(duì)比可以看出,給extends參數(shù)套上元組可以避免分發(fā)的特性,從而得到期望的結(jié)果;
- 從Eg3和Eg4對(duì)比可以看出,通過(guò)判斷兩個(gè)類(lèi)型互相是否兼容的方式,可以得到從屬類(lèi)型的正確相等判斷。
- 從Eg5和Eg6對(duì)比可以看出,非strictNullChecks模式下,undefined和null可以賦值給其他類(lèi)型的特性,導(dǎo)致number | undefined, number是兼容的,因?yàn)槭欠莝trictNullChecks模式,所以有這個(gè)結(jié)果也是符合預(yù)期。如果不需要此兼容結(jié)果,完全可以開(kāi)啟strictNullChecks模式。
最后,同理想得到OmitByValue和OmitByValueExact基本一樣的思路就不多說(shuō)了,大家可以自己思考實(shí)現(xiàn)。
Intersection
Intersection<T, U>從T中提取存在于U中的key和對(duì)應(yīng)的類(lèi)型。(注意,最終是從T中提取key和類(lèi)型)
/**
* 核心思路利用Pick提取指定的key組成的類(lèi)型
*/
type Intersection<T extends object, U extends object> = Pick<T,
Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>
type Eg = Intersection<{key1: string}, {key1:string, key2: number}>
- 約束T和U都是object,然后利用Pick提取指定的key組成的類(lèi)型
- 通過(guò)Extract<keyof T, keyof U>提取同時(shí)存在于T和U中的key,Extract<keyof U, keyof T>也是同樣的操作
那么為什么要做2次Extract然后再交叉類(lèi)型呢?原因還是在于處理類(lèi)型的兼容推導(dǎo)問(wèn)題,還記得string可分配給string | number的兼容吧:
type A = {
[p: string]: 2
}
type B = {
aaa: 2
}
// string | number
type AKEY = keyof A;
// "aaa"
type BKEY = keyof B;
// 1
type D = BKEY extends AKEY ? 1 : 2;
// 2
type F = AKEY extends BKEY ? 1 : 2;
擴(kuò)展:
定義Diff<T, U>,從T中排除存在于U中的key和類(lèi)型。
type Diff<T extends object, U extends object> = Pick<
T,
Exclude<keyof T, keyof U>
>;
Overwrite 和 Assign
Overwrite<T, U>從U中的同名屬性的類(lèi)型覆蓋T中的同名屬性類(lèi)型。(后者中的同名屬性覆蓋前者)
/**
* Overwrite實(shí)現(xiàn)
* 獲取前者獨(dú)有的key和類(lèi)型,再取兩者共有的key和該key在后者中的類(lèi)型,最后合并。
*/
type Overwrite<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;
/**
* @example
* type Eg1 = { key1: number; }
*/
type Eg1 = Overwrite<{key1: string}, {key1: number, other: boolean}>
- 首先約束T和U這兩個(gè)參數(shù)都是object
- 借助一個(gè)參數(shù)I的默認(rèn)值作為實(shí)現(xiàn)過(guò)程,使用的時(shí)候不需要傳遞I參數(shù)(只是輔助實(shí)現(xiàn)的)
- 通過(guò)Diff<T, U>獲取到存在于T但是不存在于U中的key和其類(lèi)型。(即獲取T自己特有key和類(lèi)型)。
- 通過(guò)Intersection<U, T>獲取U和T共有的key已經(jīng)該key在U中的類(lèi)型。即獲取后者同名key已經(jīng)類(lèi)型。
- 最后通過(guò)交叉類(lèi)型進(jìn)行合并,從而曲線救國(guó)實(shí)現(xiàn)了覆蓋操作。
擴(kuò)展:如何實(shí)現(xiàn)一個(gè)Assign<T, U>(類(lèi)似于Object.assign())用于合并呢?
// 實(shí)現(xiàn)
type Assign<
T extends object,
U extends object,
I = Diff<T, U> & U
> = Pick<I, keyof I>;
/**
* @example
* type Eg = {
* name: string;
* age: string;
* other: string;
* }
*/
type Eg = Assign<
{ name: string; age: number; },
{ age: string; other: string; }
>;
想一下,是不是就是先找到前者獨(dú)有的key和類(lèi)型,再和U交叉。
DeepRequired
DeepRequired<T>將T轉(zhuǎn)換成必須屬性。如果T為對(duì)象,則將遞歸對(duì)象將所有key轉(zhuǎn)換成required,類(lèi)型轉(zhuǎn)換為NonUndefined;如果T為數(shù)組則遞歸遍歷數(shù)組將每一項(xiàng)設(shè)置為NonUndefined。
/**
* DeepRequired實(shí)現(xiàn)
*/
type DeepRequired<T> = T extends (...args: any[]) => any
? T
: T extends Array<any>
? _DeepRequiredArray<T[number]>
: T extends object
? _DeepRequiredObject<T>
: T;
// 輔助工具,遞歸遍歷數(shù)組將每一項(xiàng)轉(zhuǎn)換成必選
interface _DeepRequiredArray<T> extends Array<DeepRequired<NonUndefined<T>>> {}
// 輔助工具,遞歸遍歷對(duì)象將每一項(xiàng)轉(zhuǎn)換成必選
type _DeepRequiredObject<T extends object> = {
[P in keyof T]-?: DeepRequired<NonUndefined<T[P]>>
}
DeepRequired利用extends判斷如果是函數(shù)或Primitive的類(lèi)型,就直接返回該類(lèi)型。
如果是數(shù)組類(lèi)型,則借助_DeepRequiredArray進(jìn)行遞歸,并且傳遞的參數(shù)為數(shù)組所有子項(xiàng)類(lèi)型組成的聯(lián)合類(lèi)型,如下:
type A = [string, number]
/**
* @description 對(duì)數(shù)組進(jìn)行number索引訪問(wèn),
* 得到的是所有子項(xiàng)類(lèi)型組成的聯(lián)合類(lèi)型
* type B = string | number
*/
type B = A[number]
_DeepRequiredArray是個(gè)接口(定義成type也可以),其類(lèi)型是Array<T>,完整的如下:
Array<
// DeepRequired的參數(shù)最終是個(gè)聯(lián)合類(lèi)型,會(huì)走DeepRequired的子類(lèi)型分發(fā)邏輯進(jìn)行遍歷
DeepRequired<
NonUndefined<
// T[number]實(shí)際類(lèi)似如下:
T<
a | b | c | ....
>
>
>
>
而此處的T則通過(guò)DeepRequired<T>進(jìn)行對(duì)每一項(xiàng)進(jìn)行遞歸;在T被使用之前,先被NonUndefined<T>處理一次,去掉無(wú)效類(lèi)型。
如果是對(duì)象類(lèi)型,則借助_DeepRequiredObject實(shí)現(xiàn)對(duì)象的遞歸遍歷。_DeepRequiredObject只是一個(gè)普通的映射類(lèi)型進(jìn)行變量,然后對(duì)每個(gè)key添加-?修飾符轉(zhuǎn)換成required類(lèi)型。
DeepReadonlyArray
DeepReadonlyArray<T>將T的轉(zhuǎn)換成只讀的,如果T為object則將所有的key轉(zhuǎn)換為只讀的,如果T為數(shù)組則將數(shù)組轉(zhuǎn)換成只讀數(shù)組。整個(gè)過(guò)程是深度遞歸的。
/**
* DeepReadonly實(shí)現(xiàn)
*/
type DeepReadonly<T> = T extends ((...args: any[]) => any) | Primitive
? T
: T extends _DeepReadonlyArray<infer U>
? _DeepReadonlyArray<U>
: T extends _DeepReadonlyObject<infer V>
? _DeepReadonlyObject<V>
: T;
/**
* 工具類(lèi)型,構(gòu)造一個(gè)只讀數(shù)組
*/
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
/**
* 工具類(lèi)型,構(gòu)造一個(gè)只讀對(duì)象
*/
type _DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
基本實(shí)現(xiàn)原理和DeepRequired一樣,但是注意infer U自動(dòng)推導(dǎo)數(shù)組的類(lèi)型,infer V推導(dǎo)對(duì)象的類(lèi)型。
UnionToIntersection
將聯(lián)合類(lèi)型轉(zhuǎn)變成交叉類(lèi)型。
type UnionToIntersection<T> = (T extends any
? (arg: T) => void
: never
) extends (arg: infer U) => void ? U : never
type Eg = UnionToIntersection<{ key1: string } | { key2: number }>
- T extends any ? (arg: T) => void : never該表達(dá)式一定走true分支,用此方式構(gòu)造一個(gè)逆變的聯(lián)合類(lèi)型(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
- 再利用第二個(gè)extends配合infer推導(dǎo)得到U的類(lèi)型,但是利用infer對(duì)協(xié)變類(lèi)型的特性得到交叉類(lèi)型。