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

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)型不同,則該keynever
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)型的分布式特性文檔

類(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) => {});

可以看到Windowlistener函數(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í)存在于TU內(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)!??!

  • nullundefined可以賦值給其他類(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}>;

可以看到:RType1RType2的參數(shù)為非只讀的屬性時(shí),R1R2的結(jié)果是一樣的;RType1RType2的參數(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ù),XY是待比較的兩個(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ū)別在于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類(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)型。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容