Hello你們好,我是愣錘。隨着Typescript不可阻擋的趨勢,相信小夥伴們或多或少的使用過Ts開發了。而Ts的使用除了基本的類型定義外,對於Ts的泛型、內置高級類型、自定義高級類型工具等會相對陌生。本文將會經過22個類型工具例子,深刻講解Ts類型工具原理和編程技巧。不扯閒篇,全程乾貨,內容很是多,想提高Ts功力的小夥伴請耐心讀下去。相信小夥伴們在讀完此文後,可以對這塊有更深刻的理解。下面,咱們開始吧~html
本文基本分爲三部分:git
extends
等),可是該部分更多的講解小夥伴們不清晰的一些特性,而基本功能則再也不贅述。更多的關鍵詞及技巧將包含在後續的例子演示中再具體講述;Pick
、Omit
等;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
可是類型不一樣,則該key
爲never
。編程
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()
})(),
}
複製代碼
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 & D
。api
表示條件判斷,若是前面的條件知足,則返回問號後的第一個參數,不然第二個。相似於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'>
複製代碼
提問:爲何A2
和A3
的值不同?安全
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
複製代碼
經過Eg3
和Eg4
來看,在Animal
和Dog
在變成數組後,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的調用,此時就會出現錯誤。
所以,Animal
和Dog
在進行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) => {});
複製代碼
能夠看到Window
的listener
函數要求參數是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 }>;
複製代碼
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'>;
複製代碼
/** * 主要實現是經過映射遍歷全部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,
}>
複製代碼
挑選一組屬性並組成一個新的類型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
複製代碼
基本和上述一樣的知識點,就再也不贅述了。
構造一個type
,key
爲聯合類型中的每一個子類型,類型爲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
string | number | symbol
擴展: 同態與非同態。劃重點!!! 劃重點!!! 劃重點!!!
Partial
、Readonly
和Pick
都屬於同態的,即其實現須要輸入類型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<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<T, U>
提取聯合類型T和聯合類型U的全部交集。
type Extract<T, U> = T extends U ? T : never;
/** * @example * type Eg = 'key1' */
type Eg = Extract<'key1' | 'key2', 'key1'>
複製代碼
Omit<T, K>
從類型T
中剔除K
中的全部屬性。
/** * 利用Pick實現Omit */
type Omit = Pick<T, Exclude<keyof T, K>>;
複製代碼
Pick
提取咱們須要的keys組成的類型Omit = Pick<T, 咱們須要的屬性聯合>
Exclude<keyof T, K>
;若是不利用Pick實現呢?
/** * 利用映射類型Omit */
type Omit2<T, K extends keyof any> = {
[P in Exclude<keyof T, K>]: T[P]
}
複製代碼
Exclude<keyof T, K>
[P in Exclude<keyof T, K>]
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
能夠獲取類的構造函數的參數類型,存在一個元組中。
/** * 核心實現仍是利用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;
複製代碼
/** * @desc 構造一個將字符串轉大寫的類型 * @example * type Eg1 = 'ABCD'; */
type Eg1 = Uppercase<'abcd'>;
複製代碼
/** * @desc 構造一個將字符串轉小大寫的類型 * @example * type Eg2 = 'abcd'; */
type Eg2 = Lowercase<'ABCD'>;
複製代碼
/** * @desc 構造一個將字符串首字符轉大寫的類型 * @example * type Eg3 = 'abcd'; */
type Eg3 = Capitalize<'Abcd'>;
複製代碼
/** * @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;
複製代碼
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
第2個參數是T & U
獲取的是全部類型的交叉類型Exclude
第一個參數則是T | U
,這是利用在聯合類型在extends中的分發特性,能夠理解爲Exclude<T, T & U> | Exclude<U, T & U>
;總結一下就是,提取存在於T
但不存在於T & U
的類型,而後再提取存在於U
但不存在於T & U
的,最後進行聯合。
獲取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>;
複製代碼
object
K in keyof T
遍歷全部的key,先經過NonUndefined<T[K]>
過濾T[K]
爲undefined | null
的類型,不符合的返回neverT[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
再敲重點!!!再敲重點!!!再敲重點!!!
null
和undefined
能夠賦值給其餘類型(開始該類型的嚴格賦值檢測除外),因此上述實現中須要使用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組成的聯合類型,無非就是K
和never
的位置不同罷了。一樣,你也能夠實現StringKeys
、NumberKeys
等等。可是記得能夠抽象個工廠類型哈:
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<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}>;
複製代碼
能夠看到:RType1
和RType2
的參數爲非只讀的屬性時,R1
和R2
的結果是同樣的;RType1
和RType2
的參數爲只讀的屬性時,獲得的結果R3是只讀的,R4
是非只讀的。因此,這裏要敲個重點了:
[P in Keyof T]
是映射類型,而映射是同態的,同態即會拷貝原有的屬性修飾符等。能夠參考R0的例子。-readonly
表示爲非只讀,或者能夠理解爲去掉只讀。對於只讀屬性加上-readonly
變成了非只讀,而對非只讀屬性加上-readonly
後仍是非只讀。一種常見的使用方式,好比你想把屬性變成都是非只讀的,不能前面不加修飾符(雖然不寫就表示非只讀),可是要考慮到同態拷貝的問題。第二步,解析IfEquals
IfEquals
用於判斷類型X
和Y
是否相同,相等則返回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
;// 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;
複製代碼
是否是很奇怪,爲何能推導出A
和B
類型是不同的?告訴你答案:
X
和Y
)僅被用於約束兩個相同的泛型函數則是相同的。這理解起來有些難以想象,或者說在邏輯上這種邏輯並不對(由於能夠舉出反例),可是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
的默認值爲X
,B
的默認值爲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
返回的P
,P
爲只讀時IfEquals
返回never
。
key
,不然類型爲never
,最後經過[keyof T]
獲得了全部非只讀key
的聯合類型。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}>
複製代碼
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
也是能夠的。
// 輔助函數,用於獲取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
的,所以上述並非精準的提取方式。若是實現精準的方式,則能夠考慮下面個這個類型工具。
/** * 核心實現 */
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>
複製代碼
Eg1
和Eg2
對比能夠看出,給extends
參數套上元組能夠避免分發的特性,從而獲得指望的結果;Eg3
和Eg4
對比能夠看出,經過判斷兩個類型互相是否兼容的方式,能夠獲得從屬類型的正確相等判斷。Eg5
和Eg6
對比能夠看出,非strictNullChecks
模式下,undefined和null能夠賦值給其餘類型的特性,致使number | undefined, number
是兼容的,由於是非strictNullChecks
模式,因此有這個結果也是符合預期。若是不須要此兼容結果,徹底能夠開啓strictNullChecks
模式。最後,同理想獲得OmitByValue
和OmitByValueExact
基本同樣的思路就很少說了,你們能夠本身思考實現。
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}>
複製代碼
T
和U
都是object
,而後利用Pick
提取指定的key
組成的類型Extract<keyof T, keyof U>
提取同時存在於T和U中的key,Extract<keyof U, keyof T>
也是一樣的操做那麼爲何要作2次Extract
而後再交叉類型呢?緣由仍是在於處理類型的兼容推導問題,還記得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<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}>
複製代碼
T
和U
這兩個參數都是object
Diff<T, U>
獲取到存在於T
可是不存在於U
中的key和其類型。(即獲取T
本身特有key
和類型)。Intersection<U, T>
獲取U
和T
共有的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<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<T>
將T
的轉換成只讀的,若是T
爲object
則將全部的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
推導對象的類型。將聯合類型轉變成交叉類型。
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
對協變類型的特性獲得交叉類型。轉載請註明做者及出處!