Hello大家好放椰,我是愣錘刑然。隨著Typescript不可阻擋的趨勢(shì)寺擂,相信小伙伴們或多或少的使用過Ts開發(fā)了。而Ts的使用除了基本的類型定義外泼掠,對(duì)于Ts的泛型怔软、內(nèi)置高級(jí)類型、自定義高級(jí)類型工具等會(huì)相對(duì)陌生择镇。本文將會(huì)通過22個(gè)類型工具例子挡逼,深入講解Ts類型工具原理和編程技巧。不扯閑篇腻豌,全程干貨家坎,內(nèi)容非常多,想提升Ts功力的小伙伴請(qǐng)耐心讀下去吝梅。相信小伙伴們?cè)谧x完此文后虱疏,能夠?qū)@塊有更深入的理解。下面苏携,我們開始吧~
本文基本分為三部分:
第一部分講解一些基本的關(guān)鍵詞的特性(比如索引查詢做瞪、索引訪問、映射兜叨、extends等)穿扳,但是該部分更多的講解小伙伴們不清晰的一些特性,而基本功能則不再贅述国旷。更多的關(guān)鍵詞及技巧將包含在后續(xù)的例子演示中再具體講述矛物;
第二部分講解Ts內(nèi)置的類型工具以及實(shí)現(xiàn)原理,比如Pick跪但、Omit等履羞;
第三部分講解自定義的工具類型,該部分也是最難的部分屡久,將通過一些復(fù)雜的類型工具示例進(jìn)行逐步剖析忆首,對(duì)于其中的晦澀的地方以及涉及的知識(shí)點(diǎn)逐步講解。此部分也會(huì)包含大量Ts類型工具的編程技巧被环,也希望通過此部分的講解糙及,小伙伴的Ts功底可以進(jìn)一步提升!
第一部分 前置內(nèi)容
keyof
索引查詢
對(duì)應(yīng)任何類型T
,keyof T
的結(jié)果為該類型上所有共有屬性key的聯(lián)合:
interface Eg1 {
name: string,
readonly age: number,
}
// T1的類型實(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]
索引訪問
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的類型組成的聯(lián)合類型唇聘;
T[keyof K]的方式,獲取到的是T中的key且同時(shí)存在于K時(shí)的類型組成的聯(lián)合類型柱搜;
注意:如果[]中的key有不存在T中的迟郎,則是any;因?yàn)閠s也不知道該key最終是什么類型聪蘸,所以是any宪肖;且也會(huì)報(bào)錯(cuò);
-
&
交叉類型注意點(diǎn)
交叉類型取的多個(gè)類型的并集健爬,但是如果相同key
但是類型不同控乾,則該key
為never
。
interface Eg1 {
name: string,
age: number,
}
interface Eg2 {
color: string,
age: string,
}
/**
* T的類型為 {name: string; age: number; age: never}
* 注意浑劳,age因?yàn)镋g1和Eg2中的類型不一致阱持,所以交叉后age的類型是never
*/
type T = Eg1 & Eg2
// 可通過如下示例驗(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,
}
注意魔熏,接口支持多重繼承,語法為逗號(hào)隔開鸽扁。如果是type實(shí)現(xiàn)繼承蒜绽,則可以使用交叉類型type A = B & C & D
。
-
表示條件類型桶现,可用于條件判斷
表示條件判斷躲雅,如果前面的條件滿足,則返回問號(hào)后的第一個(gè)參數(shù)骡和,否則第二個(gè)相赁。類似于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'>
提問:為什么A2和A3的值不一樣慰于?
- 如果用于簡(jiǎn)單的條件判斷钮科,則是直接判斷前面的類型是否可分配給后面的類型
- 若extends前面的類型是泛型,且泛型傳入的是聯(lián)合類型時(shí)婆赠,則會(huì)依次判斷該聯(lián)合類型的所有子類型是否可分配給extends后面的類型(是一個(gè)分發(fā)的過程)绵脯。
總結(jié),就是extends前面的參數(shù)為聯(lián)合類型時(shí)則會(huì)分解(依次遍歷所有的子類型進(jìn)行條件判斷)聯(lián)合類型進(jìn)行判斷休里。然后將最終的結(jié)果組成新的聯(lián)合類型蛆挫。
阻止extends關(guān)鍵詞對(duì)于聯(lián)合類型的分發(fā)特性
如果不想被分解(分發(fā)),做法也很簡(jiǎn)單妙黍,可以通過簡(jiǎn)單的元組類型包裹以下:
type P<T> = [T] extends ['x'] ? 1 : 2;
/**
* type A4 = 2;
*/
type A4 = P<'x' | 'y'>
類型兼容性
集合論中悴侵,如果一個(gè)集合的所有元素在集合B中都存在,則A是B的子集拭嫁;
類型系統(tǒng)中可免,如果一個(gè)類型的屬性更具體抓于,則該類型是子類型。(因?yàn)閷傩愿賱t說明該類型約束的更寬泛巴元,是父類型)
因此毡咏,我們可以得出基本的結(jié)論:子類型比父類型更加具體,父類型比子類型更寬泛。 下面我們也將基于類型的可復(fù)制性(可分配性)逮刨、協(xié)變呕缭、逆變、雙向協(xié)變等進(jìn)行進(jìn)一步的講解修己。
- 可賦值性
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
let a: Animal;
let b: Dog;
// 可以賦值姿染,子類型更佳具體,可以賦值給更佳寬泛的父類型
a = b;
// 反過來不行
b = a;
- 可賦值性在聯(lián)合類型中的特性
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;
// 不可賦值
b = a;
// 可以賦值
a = b;
是不是A的類型更多钻注,A就是子類型呢劲蜻?恰恰相反,A此處類型更多但是其表達(dá)的類型更寬泛尤辱,所以A是父類型砂豌,B是子類型。
因此b = a不成立(父類型不能賦值給子類型)光督,而a = b成立(子類型可以賦值給父類型)
- 協(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
通過Eg3和Eg4來看结借,在Animal和Dog在變成數(shù)組后筐摘,Array<Dog>依舊可以賦值給Array<Animal>,因此對(duì)于type MakeArray = Array<any>來說就是協(xié)變的船老。
最后引用維基百科中的定義:
協(xié)變與逆變(Covariance and contravariance )是在計(jì)算機(jī)科學(xué)中咖熟,描述具有父/子型別關(guān)系的多個(gè)型別通過型別構(gòu)造器、構(gòu)造出的多個(gè)復(fù)雜型別之間是否有父/子型別關(guān)系的用語柳畔。
簡(jiǎn)單說就是馍管,具有父子關(guān)系的多個(gè)類型,在通過某種構(gòu)造關(guān)系構(gòu)造成的新的類型荸镊,如果還具有父子關(guān)系則是協(xié)變的咽斧,而關(guān)系逆轉(zhuǎn)了(子變父,父變子)就是逆變的躬存≌湃牵可能聽起來有些抽象,下面我們將用更具體的例子進(jìn)行演示說明:
- 逆變
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;
// 反過來可以
Eg2 = Eg1;
理論上宛逗,Animal = Dog
是類型安全的,那么AnimalFn = DogFn
也應(yīng)該類型安全才對(duì)盾剩,為什么Ts認(rèn)為不安全呢雷激?看下面的例子:
let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => {
arg.break();
}
// 假設(shè)類型安全可以賦值
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類型(而不是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ù)或者反過來時(shí)才能賦值成功喉恋。
這是不穩(wěn)定的,因?yàn)檎{(diào)用者可能傳入了一個(gè)具有更精確類型信息的函數(shù)母廷,但是調(diào)用這個(gè)傳入的函數(shù)的時(shí)候卻使用了不是那么精確的類型信息(典型的就是上述的逆變)轻黑。 但是實(shí)際上,這極少會(huì)發(fā)生錯(cuò)誤琴昆,并且能夠?qū)崿F(xiàn)很多JavaScript里的常見模式:
// 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
子類型。但是這里可以正常使用椎咧,正是其默認(rèn)行為是雙向協(xié)變的原因“呀椋可以通過tsconfig.js
中修改strictFunctionType
屬性來嚴(yán)格控制協(xié)變和逆變勤讽。
敲重點(diǎn)!^痔摺脚牍!敲重點(diǎn)!3彩诸狭!敲重點(diǎn)!>摇驯遇!
infer
關(guān)鍵詞的功能暫時(shí)先不做太詳細(xì)的說明了,主要是用于extends
的條件類型中讓Ts自己推到類型蓄髓,具體的可以查閱官網(wǎng)叉庐。但是關(guān)于infer
的一些容易讓人忽略但是非常重要的特性,這里必須要提及一下:
-
infer
推導(dǎo)的名稱相同并且都處于逆變的位置会喝,則推導(dǎo)的結(jié)果將會(huì)是交叉類型陡叠。
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)合類型。
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)置類型工具原理解析
Partial實(shí)現(xiàn)原理解析
Partial<T>
將T
的所有屬性變成可選的枉阵。
/**
* 核心實(shí)現(xiàn)就是通過映射類型遍歷T上所有的屬性译红,
* 然后將每個(gè)屬性設(shè)置為可選屬性
*/
type Partial<T> = {
[P in keyof T]?: T[P];
}
-
[P in keyof T]
通過映射類型,遍歷T上的所有屬性 -
?:
設(shè)置為屬性為可選的 -
T[P]
設(shè)置類型為原來的類型
擴(kuò)展一下兴溜,將制定的key變成可選類型:
/**
* 主要通過K extends keyof T約束K必須為keyof T的子類型
* keyof T得到的是T的所有key組成的聯(lián)合類型
*/
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)是通過映射遍歷所有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è)新的類型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
基本和上述同樣的知識(shí)點(diǎn)昵慌,就不再贅述了假夺。
Record
構(gòu)造一個(gè)type
,key
為聯(lián)合類型中的每個(gè)子類型斋攀,類型為T
已卷。文字不好理解,先看例子:
/**
* @example
* type Eg1 = {
* a: { key1: string; };
* b: { key1: string; };
* }
* @desc 就是遍歷第一個(gè)參數(shù)'a' | 'b'的每個(gè)子類型淳蔼,然后將值設(shè)置為第二參數(shù)
*/
type Eg1 = Record<'a' | 'b', {key1: string}>
Record具體實(shí)現(xiàn):
/**
* 核心實(shí)現(xiàn)就是遍歷K侧蘸,將值設(shè)置為T
*/
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
- 原因在于類型key的類型只能為
string | number | symbol
擴(kuò)展: 同態(tài)與非同態(tài)。劃重點(diǎn)p睦妗;浒! 劃重點(diǎn)4嬖怼I卫ぁ! 劃重點(diǎn)5┐V璨ぁ!
- Partial疤孕、Readonly和Pick都屬于同態(tài)的商乎,即其實(shí)現(xiàn)需要輸入類型T來拷貝屬性,因此屬性修飾符(例如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為輸入的類型颖系,而keyof T
則遍歷了輸入類型嗅剖;而Record
的實(shí)現(xiàn)中,并沒有遍歷所有輸入的類型嘁扼,K只是約束為keyof any
的子類型即可信粮。
最后再類比一下Pick、Partial趁啸、readonly
這幾個(gè)類型工具强缘,無一例外,都是使用到了keyof T
來輔助拷貝傳入類型的屬性不傅。
Exclude原理解析
Exclude<T, U>提取存在于T旅掂,但不存在于U的類型組成的聯(lián)合類型。
/**
* 遍歷T中的所有子類型访娶,如果該子類型約束于U(存在于U商虐、兼容于U),
* 則返回never類型崖疤,否則返回該子類型
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* @example
* type Eg = 'key1'
*/
type Eg = Exclude<'key1' | 'key2', 'key2'>
敲重點(diǎn)C爻怠!劫哼!
-
never
表示一個(gè)不存在的類型 -
never
與其他類型的聯(lián)合后叮趴,是沒有never
的
/**
* @example
* type Eg2 = string | number
*/
type Eg2 = string | number | never
因此上述Eg其實(shí)就等于key1 | never
,也就是type Eg = key1
Extract
Extract<T, U>提取聯(lián)合類型T和聯(lián)合類型U的所有交集。
type Extract<T, U> = T extends U ? T : never;
/**
* @example
* type Eg = 'key1'
*/
type Eg = Extract<'key1' | 'key2', 'key1'>
Omit原理解析
Omit<T, K>
從類型T
中剔除K
中的所有屬性权烧。
/**
* 利用Pick實(shí)現(xiàn)Omit
*/
type Omit = Pick<T, Exclude<keyof T, K>>;
- 換種思路想一下眯亦,其實(shí)現(xiàn)可以是利用
Pick
提取我們需要的keys組成的類型 - 因此也就是
Omit = Pick<T, 我們需要的屬性聯(lián)合>
- 而我們需要的屬性聯(lián)合就是,從
T
的屬性聯(lián)合中排出存在于聯(lián)合類型K
中的 - 因此也就是
Exclude<keyof T, K>
;
如果不利用Pick實(shí)現(xiàn)呢?
/**
* 利用映射類型Omit
*/
type Omit2<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P]
}
- 其實(shí)現(xiàn)類似于
Pick
的原理實(shí)現(xiàn) - 區(qū)別在于是遍歷的我們需要的屬性不一樣
- 我們需要的屬性和上面的例子一樣般码,就是
Exclude<keyof T, K>
- 因此搔驼,遍歷就是
[P in Exclude<keyof T, K>]
Parameters 和 ReturnType
Parameters 獲取函數(shù)的參數(shù)類型,將每個(gè)參數(shù)類型放在一個(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ù)類型,所以(...args: any) => any>
替換成Function
也是可以的 - 具體實(shí)現(xiàn)就是糯耍,判斷T是否是函數(shù)類型扔字,如果是則使用
inter P
讓ts自己推導(dǎo)出函數(shù)的參數(shù)類型,并將推導(dǎo)的結(jié)果存到類型P
上温技,否則就返回never
革为;
敲重點(diǎn)!6媪邸震檩!敲重點(diǎn)!!抛虏!敲重點(diǎn)2┢洹!迂猴!
-
infer
關(guān)鍵詞作用是讓Ts自己推導(dǎo)類型慕淡,并將推導(dǎo)結(jié)果存儲(chǔ)在其參數(shù)綁定的類型上。Eg:infer P
就是將結(jié)果存在類型P
上沸毁,供使用峰髓。 -
infer
關(guān)鍵詞只能在extends
條件類型上使用,不能在其他地方使用息尺。
再敲重點(diǎn)P!搂誉!再敲重點(diǎn)!!至非!再敲重點(diǎn)Hせ荨J躺揽思!
type Eg = [arg1: string, arg2: number]
這是一個(gè)元組福侈,但是和我們常見的元組type tuple = [string, number]
伟墙。官網(wǎng)未提到該部分文檔說明噩翠,其實(shí)可以把這個(gè)作為類似命名元組剂娄,或者具名元組的意思去理解惕它。實(shí)質(zhì)上沒有什么特殊的作用,比如無法通過這個(gè)具名去取值不行的缸废。但是從語義化的角度姆泻,個(gè)人覺得多了語義化的表達(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ù)組所有元素的類型:
/**
* 約束參數(shù)T為數(shù)組類型四苇,
* 判斷T是否為數(shù)組,如果是數(shù)組類型則推導(dǎo)數(shù)組元素的類型
*/
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ù)的返回值類型方咆。
/**
* @desc ReturnType的實(shí)現(xiàn)其實(shí)和Parameters的基本一樣
* 無非是使用infer R的位置不一樣月腋。
*/
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
ConstructorParameters
ConstructorParameters
可以獲取類的構(gòu)造函數(shù)的參數(shù)類型,存在一個(gè)元組中瓣赂。
/**
* 核心實(shí)現(xiàn)還是利用infer進(jìn)行推導(dǎo)構(gòu)造函數(shù)的參數(shù)類型
*/
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ù)的類榆骚。注意這里有個(gè)
abstract
修飾符,等下會(huì)說明煌集。 - 實(shí)現(xiàn)時(shí)妓肢,判斷T是滿足約束的類時(shí),利用
infer P
自動(dòng)推導(dǎo)構(gòu)造函數(shù)的參數(shù)類型苫纤,并最終返回該類型碉钠。
敲重點(diǎn)8倩骸!喊废!敲重點(diǎn)W8摺!污筷!敲重點(diǎn)9す搿!瓣蛀!
那么疑問來了陆蟆,為什么要對(duì)T要約束為abstract抽象類呢?看下面例子:
/**
* 定義一個(gè)普通類
*/
class MyClass {}
/**
* 定義一個(gè)抽象類
*/
abstract class MyAbstractClass {}
// 可以賦值
const c1: typeof MyClass = MyClass
// 報(bào)錯(cuò)揪惦,無法將抽象構(gòu)造函數(shù)類型分配給非抽象構(gòu)造函數(shù)類型
const c2: typeof MyClass = MyAbstractClass
// 可以賦值
const c3: typeof MyAbstractClass = MyClass
// 可以賦值
const c4: typeof MyAbstractClass = MyAbstractClass
由此看出遍搞,如果將類型定義為抽象類(抽象構(gòu)造函數(shù)),則既可以賦值為抽象類器腋,也可以賦值為普通類溪猿;而反之則不行。
再敲重點(diǎn)H宜U锵亍!再敲重點(diǎn)4胱蟆R廊!再敲重點(diǎn)T跖P剜摇!
這里繼續(xù)提問凉逛,直接使用類作為類型性宏,和使用typeof
類作為類型,有什么區(qū)別呢状飞?
/**
* 定義一個(gè)類
*/
class People {
name: number;
age: number;
constructor() {}
}
// p1可以正常賦值
const p1: People = new People();
// 等號(hào)后面的People報(bào)錯(cuò)毫胜,類型“typeof People”缺少類型“People”中的以下屬性: name, age
const p2: People = People;
// p3報(bào)錯(cuò),類型 "People" 中缺少屬性 "prototype"诬辈,但類型 "typeof People" 中需要該屬性
const p3: typeof People = new People();
// p4可以正常賦值
const p4: typeof People = People;
結(jié)論是這樣的:
- 當(dāng)把類直接作為類型時(shí)酵使,該類型約束的是該類型必須是類的實(shí)例;即該類型獲取的是該類上的實(shí)例屬性和實(shí)例方法(也叫原型方法)焙糟;
- 當(dāng)把typeof 類作為類型時(shí)口渔,約束的滿足該類的類型;即該類型獲取的是該類上的靜態(tài)屬性和方法穿撮。
最后缺脉,只需要對(duì)infer
的使用換個(gè)位置瞧哟,便可以獲取構(gòu)造函數(shù)返回值的類型:
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)的類型
Uppercase
/**
* @desc 構(gòu)造一個(gè)將字符串轉(zhuǎn)大寫的類型
* @example
* type Eg1 = 'ABCD';
*/
type Eg1 = Uppercase<'abcd'>;
Lowercase
/**
* @desc 構(gòu)造一個(gè)將字符串轉(zhuǎn)小大寫的類型
* @example
* type Eg2 = 'abcd';
*/
type Eg2 = Lowercase<'ABCD'>;
Capitalize
/**
* @desc 構(gòu)造一個(gè)將字符串首字符轉(zhuǎn)大寫的類型
* @example
* type Eg3 = 'Abcd';
*/
type Eg3 = Capitalize<'abcd'>;
Uncapitalize
/**
* @desc 構(gòu)造一個(gè)將字符串首字符轉(zhuǎn)小寫的類型
* @example
* type Eg3 = 'aBCD';
*/
type Eg3 = Uncapitalize<'ABCD'>;
這些類型工具,在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í)類型工具及類型編程技巧
SymmetricDifference
SymmetricDifference<T, U>
獲取沒有同時(shí)存在于T
和U
內(nè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)合類型、交叉類型和Exclude咧党。
- 首先利用
Exclude
從獲取存在于第一個(gè)參數(shù)但是不存在于第二個(gè)參數(shù)的類型 -
Exclude
第2個(gè)參數(shù)是T & U
獲取的是所有類型的交叉類型 -
Exclude
第一個(gè)參數(shù)則是T | U
秘蛔,這是利用在聯(lián)合類型在extends
中的分發(fā)特性,可以理解為Exclude<T, T & U> | Exclude<U, T & U>
;
總結(jié)一下就是傍衡,提取存在于T
但不存在于T & U
的類型深员,然后再提取存在于U但不存在于T & U
的,最后進(jìn)行聯(lián)合蛙埂。
FunctionKeys
獲取T中所有類型為函數(shù)的key組成的聯(lián)合類型倦畅。
/**
* @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
類型為object
- 通過映射類型
K in keyof T
遍歷所有的key
,先通過NonUndefined<T[K]>
過濾T[K]
為undefined | null
的類型绣的,不符合的返回never - 若
T[K]
為有效類型叠赐,則判斷是否為Function
類型,是的話返回K
,否則never
屡江;此時(shí)可以得到的類型芭概,例如:
/**
* 上述的Eg在此時(shí)應(yīng)該是如下類型,偽代碼:
*/
type TempType = {
key1: never,
key2: 'key2',
key3: 'key3',
}
最后經(jīng)過{省略}[keyof T]
索引訪問惩嘉,取到的為值類型的聯(lián)合類型never | key2 | key3
,計(jì)算后就是key2 | key3
;
敲重點(diǎn)0罩蕖!文黎!敲重點(diǎn)H敲纭!耸峭!敲重點(diǎn)W亍!抓艳!
-
T[]
是索引訪問操作触机,可以取到值的類型 -
T['a' | 'b']
若[]內(nèi)參數(shù)是聯(lián)合類型,則也是分發(fā)索引的特性玷或,依次取到值的類型進(jìn)行聯(lián)合 -
T[keyof T]
則是獲取T所有值的類型類型儡首; -
never
和其他類型進(jìn)行聯(lián)合時(shí),never
是不存在的偏友。例如:never | number | string
等同于number | string
再敲重點(diǎn)J呖琛!位他!再敲重點(diǎn)7毡簟2 !再敲重點(diǎn)N韪汀>┚啊!
-
null
和undefined
可以賦值給其他類型(開始該類型的嚴(yán)格賦值檢測(cè)除外),所以上述實(shí)現(xiàn)中需要使用`NonUndefined先行判斷骗奖。 - NonUndefined中的實(shí)現(xiàn)确徙,只判斷了
T extends undefined
,其實(shí)也是因?yàn)閮烧呖梢曰ハ嗉嫒莸闹醋馈K阅銚Q成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;
最后,如果你想寫一個(gè)獲取非函數(shù)類型的key組成的聯(lián)合類型仰挣,無非就是K和never的位置不一樣罷了伴逸。同樣,你也可以實(shí)現(xiàn)StringKeys膘壶、NumberKeys等等错蝴。但是記得可以抽象個(gè)工廠類型哈:
type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined;
/**
* @desc 用于創(chuàng)建獲取指定類型工具的類型工廠
* @param T 待提取的類型
* @param P 要?jiǎng)?chuàng)建的類型
* @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就可以通過工廠類型進(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
所有非只讀類型的key
組成的聯(lián)合類型。
/**
* 核心實(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è)輔助類型香椎,判斷X和Y是否類型相同漱竖,
* @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è)步驟:
第一步,先理解只讀和非只讀的一些特性
/**
* 遍歷類型T玛界,原封不動(dòng)的返回万矾,有點(diǎn)類似于拷貝類型的意思
*/
type RType1<T> = {
[P in keyof T]: T[P];
}
/**
* 遍歷類型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]
是映射類型严嗜,而映射是同態(tài)的,同態(tài)即會(huì)拷貝原有的屬性修飾符等洲敢÷可以參考R0的例子。 - 映射類型上的
-readonly
表示為非只讀,或者可以理解為去掉只讀睦优。對(duì)于只讀屬性加上-readonly
變成了非只讀渗常,而對(duì)非只讀屬性加上-readonly
后還是非只讀。一種常見的使用方式汗盘,比如你想把屬性變成都是非只讀的皱碘,不能前面不加修飾符(雖然不寫就表示非只讀),但是要考慮到同態(tài)拷貝的問題隐孽。
第二步尸执,解析IfEquals
IfEquals
用于判斷類型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è)類型掂之,如果相等則返回A
抗俄,不相等返回B
。 -
IfEquals
的基本骨架是type IfEquals<> = (參數(shù)1) extends (參數(shù)2) ? A : B
這樣的世舰,就是判斷如果參數(shù)1的類型能夠分配給參數(shù)2的類型动雹,則返回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
類型 - 通過映射類型
[P in keyof T]
進(jìn)行遍歷震蒋,key對(duì)應(yīng)的值則是IfEquals<類型1, 類型2, P>
茸塞,如果類型1和類型2相等則返回對(duì)應(yīng)的P(也就是key),否則返回never查剖。
而P其實(shí)就是一個(gè)只有一個(gè)當(dāng)前key的聯(lián)合類型钾虐,所以[Q in P]: T[P]也只是一個(gè)普通的映射類型。但是要注意的是參數(shù)1{ [Q in P]: T[P] }是通過{}構(gòu)造的一個(gè)類型笋庄,參數(shù)2{ -readonly [Q in P]: T[P] }也是通過{}構(gòu)造的一個(gè)類型,兩者的唯一區(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í)弯院,類型為key,否則類型為never泪掀,最后通過[keyof T]得到了所有非只讀key的聯(lián)合類型听绳。
OptionalKeys
OptionalKeys<T>提取T中所有可選類型的key組成的聯(lián)合類型。
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)异赫,用映射類型遍歷所有key椅挣,通過Pick<T, P>提取當(dāng)前key和類型。注意塔拳,這里也是利用了同態(tài)拷貝會(huì)拷貝可選修飾符的特性鼠证。
利用{} extends {當(dāng)前key: 類型}判斷是否是可選類型。
// Eg2 = false
type Eg2 = {} extends {key1: string} ? true : false;
// Eg3 = true
type Eg3 = {} extends {key1?: string} ? true : false;
利用的就是{}和只包含可選參數(shù)類型{key?: string}是兼容的這一特性靠抑。把extends前面的{}替換成object也是可以的量九。
增強(qiáng)Pick
PickByValue提取指定值的類型
// 輔助函數(shù),用于獲取T中類型不為never的類型組成的聯(lián)合類型
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的類型兼容特性颂碧,所以類似string是可以分配給string | number的荠列,因此上述并不是精準(zhǔn)的提取方式。如果實(shí)現(xiàn)精準(zhǔn)的方式载城,則可以考慮下面?zhèn)€這個(gè)類型工具肌似。
PickByValueExact精準(zhǔn)的提取指定值的類型
/**
* 核心實(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)的類型
二是利用給泛型套一層元組規(guī)避extends的分發(fā)式聯(lián)合類型的特性
三是利用兩個(gè)類型互相兼容的方式判斷是否相同。
具體可以看下下面例子:
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ì)比可以看出猴贰,通過判斷兩個(gè)類型互相是否兼容的方式对雪,可以得到從屬類型的正確相等判斷。
- 從Eg5和Eg6對(duì)比可以看出米绕,非strictNullChecks模式下瑟捣,undefined和null可以賦值給其他類型的特性,導(dǎo)致number | undefined, number是兼容的栅干,因?yàn)槭欠莝trictNullChecks模式迈套,所以有這個(gè)結(jié)果也是符合預(yù)期。如果不需要此兼容結(jié)果碱鳞,完全可以開啟strictNullChecks模式桑李。
最后,同理想得到OmitByValue和OmitByValueExact基本一樣的思路就不多說了,大家可以自己思考實(shí)現(xiàn)贵白。
Intersection
Intersection<T, U>從T中提取存在于U中的key和對(duì)應(yīng)的類型率拒。(注意,最終是從T中提取key和類型)
/**
* 核心思路利用Pick提取指定的key組成的類型
*/
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組成的類型
- 通過Extract<keyof T, keyof U>提取同時(shí)存在于T和U中的key猬膨,Extract<keyof U, keyof T>也是同樣的操作
那么為什么要做2次Extract然后再交叉類型呢?原因還是在于處理類型的兼容推導(dǎo)問題呛伴,還記得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和類型。
type Diff<T extends object, U extends object> = Pick<
T,
Exclude<keyof T, keyof U>
>;
Overwrite 和 Assign
Overwrite<T, U>從U中的同名屬性的類型覆蓋T中的同名屬性類型热康。(后者中的同名屬性覆蓋前者)
/**
* Overwrite實(shí)現(xiàn)
* 獲取前者獨(dú)有的key和類型沛申,再取兩者共有的key和該key在后者中的類型,最后合并姐军。
*/
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)過程污它,使用的時(shí)候不需要傳遞I參數(shù)(只是輔助實(shí)現(xiàn)的)
- 通過Diff<T, U>獲取到存在于T但是不存在于U中的key和其類型。(即獲取T自己特有key和類型)庶弃。
- 通過Intersection<U, T>獲取U和T共有的key已經(jīng)該key在U中的類型。即獲取后者同名key已經(jīng)類型德澈。
- 最后通過交叉類型進(jìn)行合并歇攻,從而曲線救國實(shí)現(xiàn)了覆蓋操作。
擴(kuò)展:如何實(shí)現(xiàn)一個(gè)Assign<T, U>(類似于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和類型,再和U交叉镇辉。
DeepRequired
DeepRequired<T>將T轉(zhuǎn)換成必須屬性屡穗。如果T為對(duì)象,則將遞歸對(duì)象將所有key轉(zhuǎn)換成required忽肛,類型轉(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的類型,就直接返回該類型罕模。
如果是數(shù)組類型评腺,則借助_DeepRequiredArray進(jìn)行遞歸,并且傳遞的參數(shù)為數(shù)組所有子項(xiàng)類型組成的聯(lián)合類型淑掌,如下:
type A = [string, number]
/**
* @description 對(duì)數(shù)組進(jìn)行number索引訪問蒿讥,
* 得到的是所有子項(xiàng)類型組成的聯(lián)合類型
* type B = string | number
*/
type B = A[number]
_DeepRequiredArray是個(gè)接口(定義成type也可以),其類型是Array<T>,完整的如下:
Array<
// DeepRequired的參數(shù)最終是個(gè)聯(lián)合類型,會(huì)走DeepRequired的子類型分發(fā)邏輯進(jìn)行遍歷
DeepRequired<
NonUndefined<
// T[number]實(shí)際類似如下:
T<
a | b | c | ....
>
>
>
>
而此處的T則通過DeepRequired<T>進(jìn)行對(duì)每一項(xiàng)進(jìn)行遞歸芋绸;在T被使用之前媒殉,先被NonUndefined<T>處理一次,去掉無效類型侥钳。
如果是對(duì)象類型适袜,則借助_DeepRequiredObject實(shí)現(xiàn)對(duì)象的遞歸遍歷。_DeepRequiredObject只是一個(gè)普通的映射類型進(jìn)行變量舷夺,然后對(duì)每個(gè)key添加-?修飾符轉(zhuǎn)換成required類型苦酱。
DeepReadonlyArray
DeepReadonlyArray<T>將T的轉(zhuǎn)換成只讀的,如果T為object則將所有的key轉(zhuǎn)換為只讀的给猾,如果T為數(shù)組則將數(shù)組轉(zhuǎn)換成只讀數(shù)組疫萤。整個(gè)過程是深度遞歸的。
/**
* 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;
/**
* 工具類型敢伸,構(gòu)造一個(gè)只讀數(shù)組
*/
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
/**
* 工具類型扯饶,構(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ù)組的類型池颈,infer V推導(dǎo)對(duì)象的類型尾序。
UnionToIntersection
將聯(lián)合類型轉(zhuǎn)變成交叉類型。
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)合類型(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
- 再利用第二個(gè)extends配合infer推導(dǎo)得到U的類型每币,但是利用infer對(duì)協(xié)變類型的特性得到交叉類型。