Ts高手篇:22個(gè)示例深入講解Ts最晦澀難懂的高級(jí)類型工具

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但是類型不同控乾,則該keynever
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) => {});

可以看到Windowlistener函數(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è)typekey為聯(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í)存在于TU內(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韪汀>┚啊!

  • nullundefined可以賦值給其他類型(開始該類型的嚴(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}>;

可以看到:RType1RType2的參數(shù)為非只讀的屬性時(shí)良狈,R1R2的結(jié)果是一樣的;RType1RType2的參數(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ù),XY是待比較的兩個(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ū)別在于XY不同胰蝠。這里看下具體下面的例子:
// 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é)變類型的特性得到交叉類型。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末琢歇,一起剝皮案震驚了整個(gè)濱河市兰怠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌李茫,老刑警劉巖揭保,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異魄宏,居然都是意外死亡秸侣,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門宠互,熙熙樓的掌柜王于貴愁眉苦臉地迎上來塔次,“玉大人,你說我怎么就攤上這事名秀±海” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵匕得,是天一觀的道長(zhǎng)继榆。 經(jīng)常有香客問我巾表,道長(zhǎng),這世上最難降的妖魔是什么略吨? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任集币,我火速辦了婚禮,結(jié)果婚禮上翠忠,老公的妹妹穿的比我還像新娘鞠苟。我一直安慰自己,他們只是感情好秽之,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布当娱。 她就那樣靜靜地躺著,像睡著了一般考榨。 火紅的嫁衣襯著肌膚如雪跨细。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天河质,我揣著相機(jī)與錄音冀惭,去河邊找鬼。 笑死掀鹅,一個(gè)胖子當(dāng)著我的面吹牛散休,可吹牛的內(nèi)容都是我干的费变。 我是一名探鬼主播不瓶,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼享潜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼支子!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奖慌,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后对人,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拂共,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年牺弄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宜狐。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡势告,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抚恒,到底是詐尸還是另有隱情咱台,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布俭驮,位于F島的核電站回溺,受9級(jí)特大地震影響春贸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜遗遵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一萍恕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧车要,春花似錦允粤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至登澜,卻和暖如春阔挠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背脑蠕。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工购撼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谴仙。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓迂求,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親晃跺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子揩局,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容