Ts高手篇:22個示例深刻講解Ts最晦澀難懂的高級類型工具

Hello你們好,我是愣錘。隨着Typescript不可阻擋的趨勢,相信小夥伴們或多或少的使用過Ts開發了。而Ts的使用除了基本的類型定義外,對於Ts的泛型、內置高級類型、自定義高級類型工具等會相對陌生。本文將會經過22個類型工具例子,深刻講解Ts類型工具原理和編程技巧。不扯閒篇,全程乾貨,內容很是多,想提高Ts功力的小夥伴請耐心讀下去。相信小夥伴們在讀完此文後,可以對這塊有更深刻的理解。下面,咱們開始吧~html

本文基本分爲三部分:git

  • 第一部分講解一些基本的關鍵詞的特性(好比索引查詢、索引訪問、映射、extends等),可是該部分更多的講解小夥伴們不清晰的一些特性,而基本功能則再也不贅述。更多的關鍵詞及技巧將包含在後續的例子演示中再具體講述;
  • 第二部分講解Ts內置的類型工具以及實現原理,好比PickOmit等;
  • 第三部分講解自定義的工具類型,該部分也是最難的部分,將經過一些複雜的類型工具示例進行逐步剖析,對於其中的晦澀的地方以及涉及的知識點逐步講解。此部分也會包含大量Ts類型工具的編程技巧,也但願經過此部分的講解,小夥伴的Ts功底能夠進一步提高!

第一部分 前置內容

  • keyof 索引查詢

對應任何類型T,keyof T的結果爲該類型上全部共有屬性key的聯合:github

interface Eg1 {
  name: string,
  readonly age: number,
}
// T1的類型實則是name | age
type T1 = keyof Eg1

class Eg2 {
  private name: string;
  public readonly age: number;
  protected home: string;
}
// T2實則被約束爲 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的類型組成的聯合類型; T[keyof K]的方式,獲取到的是T中的key且同時存在於K時的類型組成的聯合類型; 注意:若是[]中的key有不存在T中的,則是any;由於ts也不知道該key最終是什麼類型,因此是any;且也會報錯;typescript

  • & 交叉類型注意點

交叉類型取的多個類型的並集,可是若是相同key可是類型不一樣,則該keynever編程

interface Eg1 {
  name: string,
  age: number,
}

interface Eg2 {
  color: string,
  age: string,
}

/** * T的類型爲 {name: string; age: number; age: never} * 注意,age由於Eg1和Eg2中的類型不一致,因此交叉後age的類型是never */
type T = Eg1 & Eg2
// 可經過以下示例驗證
const val: T = {
  name: '',
  color: '',
  age: (function a() {
    throw Error()
  })(),
}
複製代碼

extends關鍵詞特性(重點)

  • 用於接口,表示繼承
interface T1 {
  name: string,
}

interface T2 {
  sex: number,
}

/** * @example * T3 = {name: string, sex: number, age: number} */
interface T3 extends T1, T2 {
  age: number,
}
複製代碼

注意,接口支持多重繼承,語法爲逗號隔開。若是是type實現繼承,則可使用交叉類型type A = B & C & Dapi

  • 表示條件類型,可用於條件判斷

表示條件判斷,若是前面的條件知足,則返回問號後的第一個參數,不然第二個。相似於js的三元運算。數組

/** * @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'>
複製代碼

提問:爲何A2A3的值不同?安全

  • 若是用於簡單的條件判斷,則是直接判斷前面的類型是否可分配給後面的類型
  • extends前面的類型是泛型,且泛型傳入的是聯合類型時,則會依次判斷該聯合類型的全部子類型是否可分配給extends後面的類型(是一個分發的過程)。

總結,就是extends前面的參數爲聯合類型時則會分解(依次遍歷全部的子類型進行條件判斷)聯合類型進行判斷。而後將最終的結果組成新的聯合類型。微信

  • 阻止extends關鍵詞對於聯合類型的分發特性

若是不想被分解(分發),作法也很簡單,能夠經過簡單的元組類型包裹如下:markdown

type P<T> = [T] extends ['x'] ? 1 : 2;
/** * type A4 = 2; */
type A4 = P<'x' | 'y'>
複製代碼

條件類型的分佈式特性文檔

類型兼容性

集合論中,若是一個集合的全部元素在集合B中都存在,則A是B的子集;

類型系統中,若是一個類型的屬性更具體,則該類型是子類型。(由於屬性更少則說明該類型約束的更寬泛,是父類型)

所以,咱們能夠得出基本的結論:子類型比父類型更加具體,父類型比子類型更寬泛。 下面咱們也將基於類型的可複製性(可分配性)、協變、逆變、雙向協變等進行進一步的講解。

  • 可賦值性
interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

let a: Animal;
let b: Dog;

// 能夠賦值,子類型更佳具體,能夠賦值給更佳寬泛的父類型
a = b;
// 反過來不行
b = a;
複製代碼
  • 可賦值性在聯合類型中的特性
type A = 1 | 2 | 3;
type B = 2 | 3;
let a: A;
let b: B;

// 不可賦值
b = a;
// 能夠賦值
a = b;
複製代碼

是否是A的類型更多,A就是子類型呢?偏偏相反,A此處類型更多可是其表達的類型更寬泛,因此A是父類型,B是子類型。

所以b = a不成立(父類型不能賦值給子類型),而a = b成立(子類型能夠賦值給父類型)。

  • 協變
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
複製代碼

經過Eg3Eg4來看,在AnimalDog在變成數組後,Array<Dog>依舊能夠賦值給Array<Animal>,所以對於type MakeArray = Array<any>來講就是協變的。

最後引用維基百科中的定義:

協變與逆變(Covariance and contravariance )是在計算機科學中,描述具備父/子型別關係的多個型別經過型別構造器、構造出的多個複雜型別之間是否有父/子型別關係的用語。

簡單說就是,具備父子關係的多個類型,在經過某種構造關係構形成的新的類型,若是還具備父子關係則是協變的,而關係逆轉了(子變父,父變子)就是逆變的。可能聽起來有些抽象,下面咱們將用更具體的例子進行演示說明:

  • 逆變
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也應該類型安全才對,爲何Ts認爲不安全呢?看下面的例子:

let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => {
  arg.break();
}

// 假設類型安全能夠賦值
animal = dog;
// 那麼animal在調用時約束的參數,缺乏dog所需的參數,此時會致使錯誤
animal({name: 'cat'});
複製代碼

從這個例子看到,若是dog函數賦值給animal函數,那麼animal函數在調用時,約束的是參數必需要爲Animal類型(而不是Dog),可是animal實際爲dog的調用,此時就會出現錯誤。

所以,AnimalDog在進行type Fn<T> = (arg: T) => void構造器構造後,父子關係逆轉了,此時成爲「逆變」。

  • 雙向協變

Ts在函數參數的比較中實際上默認採起的策略是雙向協變:只有當源函數參數可以賦值給目標函數或者反過來時才能賦值成功。

這是不穩定的,由於調用者可能傳入了一個具備更精確類型信息的函數,可是調用這個傳入的函數的時候卻使用了不是那麼精確的類型信息(典型的就是上述的逆變)。 可是實際上,這極少會發生錯誤,而且可以實現不少JavaScript裏的常見模式:

// lib.dom.d.ts中EventListener的接口定義
interface EventListener {
  (evt: Event): void;
}
// 簡化後的Event
interface Event {
  readonly target: EventTarget | null;
  preventDefault(): void;
}
// 簡化合並後的MouseEvent
interface MouseEvent extends Event {
  readonly x: number;
  readonly y: number;
}

// 簡化後的Window接口
interface Window {
  // 簡化後的addEventListener
  addEventListener(type: string, listener: EventListener)
}

// 平常使用
window.addEventListener('click', (e: Event) => {});
window.addEventListener('mouseover', (e: MouseEvent) => {});
複製代碼

能夠看到Windowlistener函數要求參數是Event,可是平常使用時更多時候傳入的是Event子類型。可是這裏能夠正常使用,正是其默認行爲是雙向協變的緣由。能夠經過tsconfig.js中修改strictFunctionType屬性來嚴格控制協變和逆變。

敲重點!!!敲重點!!!敲重點!!!

infer關鍵詞的功能暫時先不作太詳細的說明了,主要是用於extends的條件類型中讓Ts本身推到類型,具體的能夠查閱官網。可是關於infer的一些容易讓人忽略可是很是重要的特性,這裏必需要說起一下:

  • infer推導的名稱相同而且都處於逆變的位置,則推導的結果將會是交叉類型
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推導的名稱相同而且都處於協變的位置,則推導的結果將會是聯合類型
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與協變逆變的參考文檔點擊這裏

企業微信截圖_8357a6f0-aa88-4faf-b21e-f1baa6bc790e.png

第二部分 Ts內置類型工具原理解析

Partial實現原理解析

Partial<T>T的全部屬性變成可選的。

/** * 核心實現就是經過映射類型遍歷T上全部的屬性, * 而後將每一個屬性設置爲可選屬性 */
type Partial<T> = {
  [P in keyof T]?: T[P];
}
複製代碼
  • [P in keyof T]經過映射類型,遍歷T上的全部屬性
  • ?:設置爲屬性爲可選的
  • T[P]設置類型爲原來的類型

擴展一下,將制定的key變成可選類型:

/** * 主要經過K extends keyof T約束K必須爲keyof T的子類型 * keyof T獲得的是T的全部key組成的聯合類型 */
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原理解析

/** * 主要實現是經過映射遍歷全部key, * 而後給每一個key增長一個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

挑選一組屬性並組成一個新的類型。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
複製代碼

基本和上述一樣的知識點,就再也不贅述了。

Record

構造一個typekey爲聯合類型中的每一個子類型,類型爲T。文字很差理解,先看例子:

/** * @example * type Eg1 = { * a: { key1: string; }; * b: { key1: string; }; * } * @desc 就是遍歷第一個參數'a' | 'b'的每一個子類型,而後將值設置爲第二參數 */
type Eg1 = Record<'a' | 'b', {key1: string}>
複製代碼

Record具體實現:

/** * 核心實現就是遍歷K,將值設置爲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

擴展: 同態與非同態。劃重點!!! 劃重點!!! 劃重點!!!

  • PartialReadonlyPick都屬於同態的,即其實現須要輸入類型T來拷貝屬性,所以屬性修飾符(例如readonly、?:)都會被拷貝。可從下面例子驗證:
/** * @example * type Eg = {readonly a?: string} */
type Eg = Pick<{readonly a?: string}, 'a'>
複製代碼

Eg的結果能夠看到,Pick在拷貝屬性時,連帶拷貝了readonly?:的修飾符。

  • Record是非同態的,不須要拷貝屬性,所以不會拷貝屬性修飾符

可能到這裏就有小夥伴疑惑了,爲何Pick拷貝了屬性,而Record沒有拷貝?咱們來對比一下其實現:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
  [P in K]: T
}
複製代碼

能夠看到Pick的實現中,注意P in K(本質是P in keyof T),T爲輸入的類型,而keyof T則遍歷了輸入類型;而Record的實現中,並無遍歷全部輸入的類型,K只是約束爲keyof any的子類型便可。

最後再類比一下Pick、Partial、readonly這幾個類型工具,無一例外,都是使用到了keyof T來輔助拷貝傳入類型的屬性。

Exclude原理解析

Exclude<T, U>提取存在於T,但不存在於U的類型組成的聯合類型。

/** * 遍歷T中的全部子類型,若是該子類型約束於U(存在於U、兼容於U), * 則返回never類型,不然返回該子類型 */
type Exclude<T, U> = T extends U ? never : T;

/** * @example * type Eg = 'key1' */
type Eg = Exclude<'key1' | 'key2', 'key2'>
複製代碼

敲重點!!!

  • never表示一個不存在的類型
  • never與其餘類型的聯合後,是沒有never
/**
 * @example
 * type Eg2 = string | number
 */
type Eg2 = string | number | never
複製代碼

所以上述Eg其實就等於key1 | never,也就是type Eg = key1

Extract

Extract<T, U>提取聯合類型T和聯合類型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實現Omit */
type Omit = Pick<T, Exclude<keyof T, K>>;
複製代碼
  • 換種思路想一下,其實現能夠是利用Pick提取咱們須要的keys組成的類型
  • 所以也就是 Omit = Pick<T, 咱們須要的屬性聯合>
  • 而咱們須要的屬性聯合就是,從T的屬性聯合中排出存在於聯合類型K中的
  • 所以也就是Exclude<keyof T, K>;

若是不利用Pick實現呢?

/** * 利用映射類型Omit */
type Omit2<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}
複製代碼
  • 其實現相似於Pick的原理實現
  • 區別在因而遍歷的咱們須要的屬性不同
  • 咱們須要的屬性和上面的例子同樣,就是Exclude<keyof T, K>
  • 所以,遍歷就是[P in Exclude<keyof T, K>]

Parameters 和 ReturnType

Parameters 獲取函數的參數類型,將每一個參數類型放在一個元組中。

/** * @desc 具體實現 */
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首先約束參數T必須是個函數類型,因此(...args: any) => any>替換成Function也是能夠的
  • 具體實現就是,判斷T是不是函數類型,若是是則使用inter P讓ts本身推導出函數的參數類型,並將推導的結果存到類型P上,不然就返回never

敲重點!!!敲重點!!!敲重點!!!

  • infer關鍵詞做用是讓Ts本身推導類型,並將推導結果存儲在其參數綁定的類型上。Eg:infer P 就是將結果存在類型P上,供使用。
  • infer關鍵詞只能在extends條件類型上使用,不能在其餘地方使用。

再敲重點!!!再敲重點!!!再敲重點!!!

  • type Eg = [arg1: string, arg2: number]這是一個元組,可是和咱們常見的元組type tuple = [string, number]。官網未提到該部分文檔說明,其實能夠把這個做爲相似命名元組,或者具名元組的意思去理解。實質上沒有什麼特殊的做用,好比沒法經過這個具名去取值不行的。可是從語義化的角度,我的以爲多了語義化的表達罷了。

  • 定義元祖的可選項,只能是最後的選項

/** * 普通方式 */
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'];
複製代碼

擴展:infer實現一個推導數組全部元素的類型:

/** * 約束參數T爲數組類型, * 判斷T是否爲數組,若是是數組類型則推導數組元素的類型 */
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 獲取函數的返回值類型。

/** * @desc ReturnType的實現其實和Parameters的基本同樣 * 無非是使用infer R的位置不同。 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
複製代碼

ConstructorParameters

ConstructorParameters能夠獲取類的構造函數的參數類型,存在一個元組中。

/** * 核心實現仍是利用infer進行推導構造函數的參數類型 */
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>
複製代碼
  • 首先約束參數T爲擁有構造函數的類。注意這裏有個abstract修飾符,等下會說明。
  • 實現時,判斷T是知足約束的類時,利用infer P自動推導構造函數的參數類型,並最終返回該類型。

敲重點!!!敲重點!!!敲重點!!!

那麼疑問來了,爲何要對T要約束爲abstract抽象類呢?看下面例子:

/** * 定義一個普通類 */
class MyClass {}
/** * 定義一個抽象類 */
abstract class MyAbstractClass {}

// 能夠賦值
const c1: typeof MyClass = MyClass
// 報錯,沒法將抽象構造函數類型分配給非抽象構造函數類型
const c2: typeof MyClass = MyAbstractClass

// 能夠賦值
const c3: typeof MyAbstractClass = MyClass
// 能夠賦值
const c4: typeof MyAbstractClass = MyAbstractClass
複製代碼

由此看出,若是將類型定義爲抽象類(抽象構造函數),則既能夠賦值爲抽象類,也能夠賦值爲普通類;而反之則不行。

再敲重點!!!再敲重點!!!再敲重點!!!

這裏繼續提問,直接使用類做爲類型,和使用typeof 類做爲類型,有什麼區別呢?

/** * 定義一個類 */
class People {
  name: number;
  age: number;
  constructor() {}
}

// p1能夠正常賦值
const p1: People = new People();
// 等號後面的People報錯,類型「typeof People」缺乏類型「People」中的如下屬性: name, age
const p2: People = People;

// p3報錯,類型 "People" 中缺乏屬性 "prototype",但類型 "typeof People" 中須要該屬性
const p3: typeof People = new People();
// p4能夠正常賦值
const p4: typeof People = People;
複製代碼

結論是這樣的:

  • 當把類直接做爲類型時,該類型約束的是該類型必須是類的實例;即該類型獲取的是該類上的實例屬性和實例方法(也叫原型方法);
  • 當把typeof 類做爲類型時,約束的知足該類的類型;即該類型獲取的是該類上的靜態屬性和方法。

最後,只須要對infer的使用換個位置,即可以獲取構造函數返回值的類型:

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
複製代碼

Ts compiler內部實現的類型

  • Uppercase
/** * @desc 構造一個將字符串轉大寫的類型 * @example * type Eg1 = 'ABCD'; */
type Eg1 = Uppercase<'abcd'>;
複製代碼
  • Lowercase
/** * @desc 構造一個將字符串轉小大寫的類型 * @example * type Eg2 = 'abcd'; */
type Eg2 = Lowercase<'ABCD'>;
複製代碼
  • Capitalize
/** * @desc 構造一個將字符串首字符轉大寫的類型 * @example * type Eg3 = 'abcd'; */
type Eg3 = Capitalize<'Abcd'>;
複製代碼
  • Uncapitalize
/** * @desc 構造一個將字符串首字符轉小寫的類型 * @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;
複製代碼

企業微信截圖_1900dfc9-3c22-4af2-9523-6860bcf03e03.png

第三部分 自定義Ts高級類型工具及類型編程技巧

SymmetricDifference

SymmetricDifference<T, U>獲取沒有同時存在於T和U內的類型。

/** * 核心實現 */
type SymmetricDifference<A, B> = SetDifference<A | B, A & B>;

/** * SetDifference的實現和Exclude同樣 */
type SymmetricDifference<T, U> = Exclude<T | U, T & U>;

/** * @example * type Eg = '1' | '4'; */
type Eg = SymmetricDifference<'1' | '2' | '3', '2' | '3' | '4'>
複製代碼

其核心實現利用了3點:分發式聯合類型、交叉類型和Exclude。

  • 首先利用Exclude從獲取存在於第一個參數可是不存在於第二個參數的類型
  • Exclude第2個參數是T & U獲取的是全部類型的交叉類型
  • Exclude第一個參數則是T | U,這是利用在聯合類型在extends中的分發特性,能夠理解爲Exclude<T, T & U> | Exclude<U, T & U>;

總結一下就是,提取存在於T但不存在於T & U的類型,而後再提取存在於U但不存在於T & U的,最後進行聯合。

FunctionKeys

獲取T中全部類型爲函數的key組成的聯合類型。

/** * @desc NonUndefined判斷T是否爲undefined */
type NonUndefined<T> = T extends undefined ? never : T;

/** * @desc 核心實現 */
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>;
複製代碼
  • 首先約束參數T類型爲object
  • 經過映射類型K in keyof T遍歷全部的key,先經過NonUndefined<T[K]>過濾T[K]undefined | null的類型,不符合的返回never
  • T[K]爲有效類型,則判斷是否爲Function類型,是的話返回K,不然never;此時能夠獲得的類型,例如:
/** * 上述的Eg在此時應該是以下類型,僞代碼: */
type TempType = {
    key1: never,
    key2: 'key2',
    key3: 'key3',
}
複製代碼
  • 最後通過{省略}[keyof T]索引訪問,取到的爲值類型的聯合類型never | key2 | key3,計算後就是key2 | key3;

敲重點!!!敲重點!!!敲重點!!!

  • T[]是索引訪問操做,能夠取到值的類型
  • T['a' | 'b'][]內參數是聯合類型,則也是分發索引的特性,依次取到值的類型進行聯合
  • T[keyof T]則是獲取T全部值的類型類型;
  • never和其餘類型進行聯合時,never是不存在的。例如:never | number | string等同於number | string

再敲重點!!!再敲重點!!!再敲重點!!!

  • nullundefined能夠賦值給其餘類型(開始該類型的嚴格賦值檢測除外),因此上述實現中須要使用NonUndefined先行判斷。
  • NonUndefined中的實現,只判斷了T extends undefined,其實也是由於二者能夠互相兼容的。因此你換成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;
複製代碼

最後,若是你想寫一個獲取非函數類型的key組成的聯合類型,無非就是Knever的位置不同罷了。一樣,你也能夠實現StringKeysNumberKeys等等。可是記得能夠抽象個工廠類型哈:

type Primitive =
  | string
  | number
  | bigint
  | boolean
  | symbol
  | null
  | undefined;

/** * @desc 用於建立獲取指定類型工具的類型工廠 * @param T 待提取的類型 * @param P 要建立的類型 * @param IsCheckNon 是否要進行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就能夠經過工廠類型進行建立了 */
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組成的聯合類型。

/** * 核心實現 */
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 一個輔助類型,判斷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的實現,咱們要分下面幾個步驟:

第一步,先理解只讀和非只讀的一些特性

/** * 遍歷類型T,原封不動的返回,有點相似於拷貝類型的意思 */
type RType1<T> = {
  [P in keyof T]: T[P];
}
/** * 遍歷類型T,將每一個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的參數爲非只讀的屬性時,R1R2的結果是同樣的;RType1RType2的參數爲只讀的屬性時,獲得的結果R3是只讀的,R4非只讀的。因此,這裏要敲個重點了:

  • [P in Keyof T]是映射類型,而映射是同態的,同態即會拷貝原有的屬性修飾符等。能夠參考R0的例子。
  • 映射類型上的-readonly表示爲非只讀,或者能夠理解爲去掉只讀。對於只讀屬性加上-readonly變成了非只讀,而對非只讀屬性加上-readonly後仍是非只讀。一種常見的使用方式,好比你想把屬性變成都是非只讀的,不能前面不加修飾符(雖然不寫就表示非只讀),可是要考慮到同態拷貝的問題。

第二步,解析IfEquals

IfEquals用於判斷類型XY是否相同,相等則返回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;
複製代碼
  • 首先IfEquals<X, Y, A, B>的四個參數,X和Y是待比較的兩個類型,若是相等則返回A,不相等返回B
  • IfEquals的基本骨架是type IfEquals<> = (參數1) extends (參數2) ? A : B這樣的,就是判斷若是參數1的類型可以分配給參數2的類型,則返回A,不然返回B;
  • 參數1和參數2的基本結構是同樣的,惟一區別在於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;
複製代碼

是否是很奇怪,爲何能推導出AB類型是不同的?告訴你答案:

  • 這是利用了Ts編譯器的一個特色,就是Ts編譯器會認爲若是兩個類型(好比這裏的XY)僅被用於約束兩個相同的泛型函數則是相同的。這理解起來有些難以想象,或者說在邏輯上這種邏輯並不對(由於能夠舉出反例),可是Ts開發團隊保證了這一特性從此不會變。可參考這裏
  • 注意,這裏也會判斷的屬性修飾符,例如readonly, 可選屬性等,看經過下面的例子驗證:
/** * T2比T1多了readonly修飾符 * T3比T1多了可選修飾符 * 這裏控制單一變量進行驗證 */
type T1 = {key1: string};
type T2 = {readonly key1: string};
type T3 = {key1?: string};

// A1 = false
type A1 = IfEquals<T1, T2, true , false>;
// A2 = false
type A2 = IfEquals<T1, T3, true , false>;
複製代碼
  • IfEquals最後就是藉助1和2來輔助判斷(語法層面的),還有就是給A的默認值爲XB的默認值爲never

最後,若是你是個愛(搞)鑽(事)研(情)的小寶寶,你或許會對我發出靈魂拷問:判斷類型是否相等(兼容)爲何不直接使用type IfEquals<X, Y, A, B> = X extends Y ? A : B呢?既簡單有粗暴(PS:來自你的邪魅一笑~)。答案,咱們看下下面的示例:

type IfEquals<X, Y, A, B> = X extends Y ? A : B;

/** * 還用上面的例子 */
type T1 = {key1: string};
type T2 = {readonly key1: string};
type T3 = {key1?: string};

// A1 = true
type A1 = IfEquals<T1, T2, true , false>;
// A2 = true
type A2 = IfEquals<T1, T3, true , false>;
複製代碼

答案顯而易見,對readonly等這些修飾符,真的無能無力了。誇爪Kill~~~

第3步,解析MutableKeys實現邏輯

  • MutableKeys首先約束T爲object類型
  • 經過映射類型[P in keyof T]進行遍歷,key對應的值則是IfEquals<類型1, 類型2, P>,若是類型1和類型2相等則返回對應的P(也就是key),不然返回never。

P其實就是一個只有一個當前key的聯合類型,因此[Q in P]: T[P]也只是一個普通的映射類型。可是要注意的是參數1{ [Q in P]: T[P] }是經過{}構造的一個類型,參數2{ -readonly [Q in P]: T[P] }也是經過{}構造的一個類型,二者的惟一區別即便-readonly

因此這裏就有意思了,回想一下上面的第一步的例子,是否是就理解了:若是P是隻讀的,那麼參數1和參數2的P最終都是隻讀的;若是P是非只讀的,則參數1的P爲非只讀的,而參數2的P-readonly去掉了非只讀屬性從而變成了只讀屬性。所以就完成了篩選:P爲非只讀時IfEquals返回的PP爲只讀時IfEquals返回never

  • 因此key爲非只讀時,類型爲key,不然類型爲never,最後經過[keyof T]獲得了全部非只讀key的聯合類型。

OptionalKeys

OptionalKeys<T>提取T中全部可選類型的key組成的聯合類型。

type OptionalKeys<T> = {
  [P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T];

type Eg = OptionalKeys<{key1?: string, key2: number}>
複製代碼
  • 核心實現,用映射類型遍歷全部key,經過Pick<T, P>提取當前key和類型。注意,這裏也是利用了同態拷貝會拷貝可選修飾符的特性。
  • 利用{} extends {當前key: 類型}判斷是不是可選類型。
// Eg2 = false
type Eg2 = {} extends {key1: string} ? true : false;
// Eg3 = true
type Eg3 = {} extends {key1?: string} ? true : false;
複製代碼

利用的就是{}和只包含可選參數類型{key?: string}是兼容的這一特性。把extends前面的{}替換成object也是能夠的。

加強Pick

  • PickByValue提取指定值的類型
// 輔助函數,用於獲取T中類型不能never的key組成的聯合類型
type TypeKeys<T> = T[keyof T];

/** * 核心實現 */
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的,所以上述並非精準的提取方式。若是實現精準的方式,則能夠考慮下面個這個類型工具。

  • PickByValueExact精準的提取指定值的類型
/** * 核心實現 */
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的核心實現主要有三點:

一是利用Pick提取咱們須要的key對應的類型

二是利用給泛型套一層元組規避extends分發式聯合類型的特性

三是利用兩個類型互相兼容的方式判斷是否相同。

具體能夠看下下面例子:

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模式下的結果
type Eg5 = Eq3<number | undefined, number>
// false,strictNullChecks模式下的結果
type Eg6 = Eq3<number | undefined, number>
複製代碼
  • Eg1Eg2對比能夠看出,給extends參數套上元組能夠避免分發的特性,從而獲得指望的結果;
  • Eg3Eg4對比能夠看出,經過判斷兩個類型互相是否兼容的方式,能夠獲得從屬類型的正確相等判斷。
  • Eg5Eg6對比能夠看出,非strictNullChecks模式下,undefined和null能夠賦值給其餘類型的特性,致使number | undefined, number是兼容的,由於是非strictNullChecks模式,因此有這個結果也是符合預期。若是不須要此兼容結果,徹底能夠開啓strictNullChecks模式。

最後,同理想獲得OmitByValueOmitByValueExact基本同樣的思路就很少說了,你們能夠本身思考實現。

Intersection

Intersection<T, U>T中提取存在於U中的key和對應的類型。(注意,最終是從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}>
複製代碼
  • 約束TU都是object,而後利用Pick提取指定的key組成的類型
  • 經過Extract<keyof T, keyof U>提取同時存在於T和U中的key,Extract<keyof U, keyof T>也是一樣的操做

那麼爲何要作2Extract而後再交叉類型呢?緣由仍是在於處理類型的兼容推導問題,還記得string可分配給string | number的兼容吧。

擴展:

定義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實現 * 獲取前者獨有的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}>
複製代碼
  • 首先約束TU這兩個參數都是object
  • 藉助一個參數I的默認值做爲實現過程,使用的時候不須要傳遞I參數(只是輔助實現的)
  • 經過Diff<T, U>獲取到存在於T可是不存在於U中的key和其類型。(即獲取T本身特有key和類型)。
  • 經過Intersection<U, T>獲取UT共有的key已經該key在U中的類型。即獲取後者同名key已經類型。
  • 最後經過交叉類型進行合併,從而曲線救國實現了覆蓋操做。

擴展:如何實現一個Assign<T, U>(相似於Object.assign())用於合併呢?

// 實現
type Assign<
  T extends object,
  U extends object,
  I = Diff<T, U> & Intersection<U, T> & Diff<U, T>
> = Pick<I, keyof I>;

/** * @example * type Eg = { * name: string; * age: string; * other: string; * } */
type Eg = Assign<
  { name: string; age: number; },
  { age: string; other: string; }
>;
複製代碼

想一下,是否是就是先找到前者獨有的key和類型,再找到二者共有的key以及該key在後者中的類型,最後找到後者獨有的key和類型,最後依次的合併進去。

DeepRequired

DeepRequired<T>將T的轉換成必須屬性。若是T爲對象,則將遞歸對象將全部key轉換成required,類型轉換爲NonUndefined;若是T爲數組則遞歸遍歷數組將每一項設置爲NonUndefined

/** * DeepRequired實現 */
type DeepRequired<T> = T extends (...args: any[]) => any
  ? T
  : T extends Array<any>
    ? _DeepRequiredArray<T[number]>
    : T extends object
      ? _DeepRequiredObject<T>
      : T;

// 輔助工具,遞歸遍歷數組將每一項轉換成必選
interface _DeepRequiredArray<T> extends Array<DeepRequired<NonUndefined<T>>> {}

// 輔助工具,遞歸遍歷對象將每一項轉換成必選
type _DeepRequiredObject<T extends object> = {
  [P in keyof T]-?: DeepRequired<NonUndefined<T[P]>>
}
複製代碼
  • DeepRequired利用extends判斷若是是函數或Primitive的類型,就直接返回該類型。
  • 若是是數組類型,則藉助_DeepRequiredArray進行遞歸,而且傳遞的參數爲數組全部子項類型組成的聯合類型,以下:
type A = [string, number]
/** * @description 對數組進行number索引訪問, * 獲得的是全部子項類型組成的聯合類型 * type B = string | number */
type B = A[number]
複製代碼
  • _DeepRequiredObject是個接口(定義成type也能夠),其類型是Array<T>;而此處的T則經過DeepRequired<T>進行對每一項進行遞歸;在T被使用以前,先被NonUndefined<T>處理一次,去掉無效類型。

  • 若是是對象類型,則藉助_DeepRequiredObject實現對象的遞歸遍歷。_DeepRequiredObject只是一個普通的映射類型進行變量,而後對每一個key添加-?修飾符轉換成required類型。

DeepReadonlyArray

DeepReadonlyArray<T>T的轉換成只讀的,若是Tobject則將全部的key轉換爲只讀的,若是T爲數組則將數組轉換成只讀數組。整個過程是深度遞歸的。

/** * DeepReadonly實現 */
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;

/** * 工具類型,構造一個只讀數組 */
interface _DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

/** * 工具類型,構造一個只讀對象 */
type _DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};
複製代碼
  • 基本實現原理和DeepRequired同樣,可是注意infer U自動推導數組的類型,infer V推導對象的類型。

UnionToIntersection

將聯合類型轉變成交叉類型。

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該表達式必定走true分支,用此方式構造一個逆變的聯合類型(arg: T1) => void | (arg: T2) => void | (arg: Tn) => void
  • 再利用第二個extends配合infer推導獲得U的類型,可是利用infer協變類型的特性獲得交叉類型

參考內容

轉載請註明做者及出處!

相關文章
相關標籤/搜索