拿下泛型财松,TS 還有什么難的嗎梆掸?
大家好扬卷,我是沐華,本文將剖析 TS 開發(fā)中常見工具類型的源碼實現(xiàn)及使用方式酸钦,并且搭配與內(nèi)容結(jié)合的練習(xí)怪得,方便大家更好的理解和掌握。本文目標(biāo):
- 更加深入的理解和掌握泛型
- 更加熟練這些內(nèi)置工具類型在項目中的運用
Exclude
Exclude<T, U>
:作用簡單說就是把 T
里面的 U
去掉卑硫,再返回 T
里還剩下的徒恋。T
和 U
必須是同種類型(具體類型/字面量類型)。如下
type T1 = Exclude<string | number, string>;
// type T1 = number;
// 上面這個肯定一看就懂欢伏,那下面這樣呢
type T2 = Exclude<'a' | 'b' | 'c', 'b' | 'd'>;
// type T2 = 'a' | 'c';
怎么就剩個 a | c
了入挣?這怎么執(zhí)行的?
先看一張圖
三元表達(dá)式大家都知道硝拧,不是返回 a
就是返回 b
径筏,這么算的話,這個 some
的類型應(yīng)該是 b
才對呀障陶,可這個結(jié)果是 a | b
又是怎么回事呢滋恬,這都是由于 TS
中的拆分或者說叫分發(fā)機制導(dǎo)致的
簡單說就是聯(lián)合類型并且是裸類型就會產(chǎn)生分發(fā),分發(fā)就會把聯(lián)合類型中的每一個類型單獨拿去判斷抱究,最后返回結(jié)果組成的聯(lián)合類型恢氯,a | b
就是這么來的,這個特性在本文后面會提到多次所以鋪墊一下鼓寺,這也是為什么反 Exclude
放在開頭的原因
結(jié)合 Exclude
的實現(xiàn)和例子來理解下
// 源碼定義
type Exclude<T, U> = T extends U ? never : T;
// 例子
type T2 = Exclude<'a' | 'b' | 'c', 'b' | 'd'>;
// type T2 = 'a' | 'c';
上面例子中的執(zhí)行邏輯:
- 由于分發(fā)會把聯(lián)合類型中的每一個類型單獨拿去判斷的原因勋拟,會先把
T
,也就是前面a | b | c
給拆分再單獨放入T extends U ? never : T
判斷 - 第一次判斷
a(T就是a)
妈候,U
就是b | d
指黎,T
并沒有繼承自U
,判斷為假州丹,返回T
也就是a
- 第二次判斷放入
b
判斷為真醋安,返回never
,ts
中的never
我們知道就是不存在值的意思墓毒,連undefined
都沒有吓揪,所以never
會被忽略崎弃,不會產(chǎn)生任何效果 - 第三次判斷放入
c
膳叨,判斷為假,和a
同理 - 最后將每一個單獨判斷的結(jié)果組成聯(lián)合類型返回走触,
never
會忽略主胧,所以就剩下a | c
總之就是:如果
T extends U
滿足分發(fā)的條件叭首,就會把所有單個類型依次放入判斷习勤,最后返回記錄的結(jié)果組合的聯(lián)合類型
Extract
Extract<T, U>
:作用是取出 T
里面的 U
,返回焙格。作用和 Exclude
剛好相反图毕,傳參也是一樣的
看例子理解 Extract
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'd'>;
// type T1 = 'a';
// 源碼定義
type Extract<T, U> = T extends U ? T : never
和 Exclude
源碼對比也只是三元表達(dá)式返回的 never : T 對調(diào)了一下,執(zhí)行原理也是一樣一樣兒的眷唉,就不重復(fù)了
Omit
Omit<T, K>
:作用是把 T(對象類型)
里邊的 K
去掉予颤,返回 T
里還剩下的
Omit
的作用和 Exclude
是一樣的,都能做類型過濾并得到新類型冬阳。
不同的是 Exclude
主要是處理聯(lián)合類型蛤虐,且會觸發(fā)分發(fā),而 Omit
主要是處理對象類型肝陪,所以自然的這倆參數(shù)也不一樣驳庭。
用法如下
// 這種場景 type 和 interface 是一樣的,后面就不重復(fù)說明了
type User = {
name: string
age: number
}
type T1 = Omit<User, 'age'>
// type T1 = { name: string }
源碼定義
// keyof any 就是 string | number | symbol
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
- 首先第一個參數(shù)
T
要傳對象類型氯窍,type
或interface
都可以 - 第二個參數(shù)
K
限制了類型只能是string | number | symbol
饲常,這一點跟js
里的對象是一個意思,對象類型的屬性名只支持這三種類型 -
in
是映射類型荞驴,用來映射遍歷枚舉類型。大白話就是循環(huán)贯城、循環(huán)語法熊楼,需要配合聯(lián)合類型來對類型進(jìn)行遍歷。in
的右邊是可遍歷的枚舉類型能犯,左邊是遍歷出來的每一項 - 用
Exclude
去除掉傳入的屬性后鲫骗,再遍歷剩下的屬性,生成新的類型返回
示例解析:
type User = {
name: string
age: number
gender: string
}
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
type T1 = Omit<User, 'age'>
// type T1 = { name: string, gender: string }
我們調(diào)用 Omit
傳入的參數(shù)是正確的踩晶,所以就分析一下后面的執(zhí)行邏輯:
Exclude<keyof T, K>
等于Exclude<'name'|'age'|'gender', 'age'>
执泰,返回的結(jié)果就是'name'|'gender
然后遍歷
'name'|'gender'
,第一次循環(huán)P
就是name
渡蜻,返回T[P]
就是User['name']
第二次循環(huán)
P
就是gender
术吝,返回T[P]
就是User['gender']
,然后循環(huán)結(jié)束結(jié)果就是
{ name: string, gender: string }
Pick
Pick<T, K>
:作用是取出 T(對象類型)
里邊兒的 K
茸苇,返回排苍。
好像和 Omit
剛好相反,Omit
是不要 K
学密,Pick
是只要 K
傳參方式和 Omit
是一樣的淘衙,就不贅述了,用法示例:
type User = {
name: string
age: number
gender: string
}
type T1 = Pick<User, 'name' | 'gender'>
// type T1 = { name: string, gender: string }
源碼定義
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
- 可以看到等號左邊做了泛型約束腻暮,限制了第二個參數(shù)
K
必須是第一個參數(shù)T
里的屬性彤守。 - 如果第二個參數(shù)傳入聯(lián)合類型毯侦,會觸發(fā)分發(fā),以此來確保準(zhǔn)確性具垫,聯(lián)合類型中的每一個單獨類型都必須是第一個對象類型中的屬性(不限制的話右邊就要出錯了)
- 參數(shù)都正確之后侈离,等號右邊的邏輯其實就是和
Omit
一模一樣的了,直接遍歷K
做修,取出返回就完事兒了
練習(xí)一
請利用本文上述內(nèi)容完成:基于如下類型霍狰,實現(xiàn)一個去掉了 gender
的新類型,實現(xiàn)方法越多越好
type User = {
name: string
age: number
gender: string
}
這個饰及?
type T1 = { name: string, age: number }
蔗坯??燎含?
我寫了幾個宾濒,歡迎補充:
type T1 = Omit<User, 'gender'>
type T2 = Pick<User, 'name' | 'age'>
type T3 = Pick<User, Exclude<keyof User, 'gender'>>
type T4 = { [P in 'name' | 'age'] : User[P] }
type T5 = { [P in Exclude<keyof User, 'gender'>] : User[P] }
Record
Record<K, T>
:作用是自定義一個對象。K
為對象的 key
或 key
的類型屏箍,T
為 value
或 value
的類型绘梦。
你有沒有這樣用過 ↓
const obj:any = {}
反正我有,其實用 Record
定義對象赴魁,在工作中還是很好用的卸奉,而且非常靈活,不同的對象定義上也會有一點區(qū)別颖御,如下
空對象
// never榄棵,會限制為空對象
// any 指的是 string | number | symbol 這幾個類型都行
type T1 = Record<any, never>
let obj1:T1 = {} // ok
// let obj1:T1 = {a:1} 這樣不行,只能是空對象
任意對象
// 任意對象潘拱,unknown 或 {} 表示對象內(nèi)容不限疹鳄,空對象也行
type T1 = Record<any, unknown>
// 或
type T1 = Record<any, {}>
let obj2:T1 = {} // ok
let obj3:T1 = {a:1} // ok
自定義對象 key
type keys = 'name' | 'age'
type T1 = Record<keys, string>
let obj1:T1 = {
name: '沐華',
age: '18'
// age: 18 報錯,第二個參數(shù) string 表示 value 值都只能是 string 類型
}
// 如果需要 value 是任意類型芦岂,下面兩個都行
type T2 = Record<keys, unknown>
type T3 = Record<keys, {}>
自定義對象 value
type keys = 'a' | 'b'
// type 或 interface 都一樣
type values<T> = {
name?: T,
age?: T,
gender?: string
}
// 自定義 value 類型
type T1 = Record<keys, values<number | string>>
let obj:T1 = {
a: { name: '沐華' },
b: { age: 18 }
}
// 固定 value 值
type T2 = Record<keys, 111>
let obj1:T2 = {
a: 111,
b: 111
}
源碼定義
type Record<K extends any, T> = { [P in K]: T; }
左邊限制了第一個參數(shù) K
只能是 string | number | symbol
類型瘪弓,可以是聯(lián)合類型,因為右邊遍歷 K
了禽最,然后遍歷出來的每個屬性的值腺怯,直接賦值為傳入的第二個參數(shù)
Partial
Partial<T>
:作用生成一個將 T(對象類型)
里所有屬性都變成可選的之后的新類型
示例如下:
type User = {
name: string
age: number
}
type T1 = Partial<User>
// 簡單說 T1 和 T2 是一模一樣的
type T2 = {
name?: string
age?: number
}
源碼定義
type Partial<T> = { [P in keyof T]?: T[P]; }
這下看源碼定義的是不是特別簡單,就是循環(huán)傳進(jìn)來的對象類型川无,給每個屬性加個 ?
變成可選屬生
Required
Required<T>
:作用和 Partial<T>
剛好相反瓢喉,Partial
是返回所有屬性都是非必填的對象類型,而 Required
則是返回所有屬性都是必填項的對象類型舀透。參數(shù) T
也是一個對象類型栓票。
示例:
type User = {
name?: string
age?: number
}
type T1 = Required<User>
// 簡單說 T1 和 T2 是一模一樣的
type T2 = {
name: string
age: number
}
源碼定義
type Required<T> = { [P in keyof T]-?: T[P]; }
和 Partial
的源碼定義相比基本一樣的,只是這里多了個減號 -
,沒錯走贪,就是減去的意思佛猛,-?
就是去掉 ?
,然后就變成必填項了坠狡,這樣解釋是不是很好理解
Readonly
Readonly<T>
:作用是返回一個所有屬性都是只讀不可修改的對象類型继找,與 Partial
和 Required
是非常相似的。參數(shù) T
也是一個對象類型逃沿。
示例:
type User = {
name: string
age?: number
}
type T1 = Readonly<User>
// 簡單說 T1 和 T2 是一模一樣的
type T2 = {
readonly name: string
readonly age?: number
}
type Readonly<T> = { readonly [P in keyof T]: T[P]; }
怎么樣婴渡?看到這是不是越發(fā)覺得源碼的類型定義越看越簡單了
我:那是不是說把所有只讀類型,全都變成非只讀就只需要 -readonly
就行了凯亮?
你:是的边臼,說得很對,就是這樣的
練習(xí)二
從上面幾個工具類型的源碼定義中我們可以發(fā)現(xiàn)假消,都只是簡單的一層遍歷柠并,就好像 js
中的淺拷貝,比如有下面這樣一個對象
type User = {
name: string
age: number
children: {
boy: number
girl: number
}
}
要把這樣一個對象所有屬性都改成可選屬性富拗,用 Partial
就行不通了臼予,它只能改變第一層,children
里的所有屬性都改不了啃沪,所以請寫一個可以實現(xiàn)的類型粘拾,功能類似深拷貝的意思
先稍微想想再往下看答案喲
寫出來一個的話,Partial
创千、Required
缰雇、 Readonly
的 “深拷貝” 類型是不是就都有了呢
想一下
// Partial 源碼定義
type Partial<T> = { [P in keyof T]?: T[P]; }
// 遞歸 Partial
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> }:T;
外層再加了一個三元表達(dá)式,如果不是對象類型直接返回签餐,如果是就遍歷寓涨;然后屬性值改成遞歸調(diào)用就可以了
// 遞歸 Required
type DeepRequired<T> = T extends object ? { [P in keyof T]-?: DeepRequired<T[P]> }:T;
// 遞歸 Readonly
type DeepReadonly<T> = T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> }:T;
NonNullable
NonNullable<T>
:作用是去掉 T
中的 null
和 undefined
盯串。T
為字面量/具體類型的聯(lián)合類型氯檐,如果是對象類型是沒有效果的。如下
type T1 = NonNullable<string | number | undefined>;
// type T1 = string | number
type T2 = NonNullable<string[] | null | undefined>;
// type T2 = string[]
type T3 = {
name: string
age: undefined
}
type T4 = NonNullable<T3> // 對象是不行的
源碼定義
// 4.8版本之前的版本
type NonNullable<T> = T extends null | undefined ? never : T;
// 4.8
type NonNullable<T> = T & {}
TS 4.8版本
之前的就是用一個三元表達(dá)式來過濾 null | undefined
体捏。而在 4.8
版本直接就是 T & {}
冠摄,這是什么原理呢?其實是因為這個版本對 --strictNullChecks
做了增加几缭,這主要體現(xiàn)還是在聯(lián)合類型和交叉類型上河泳,為什么這么說?
在 js
中都知道萬物皆對象年栓,原型鏈的最終點的正常對象就是 Object
了(null
算不正常的)拆挥,數(shù)據(jù)類型都是在原型鏈中繼承于 Object
派生出來的。
在 ts
中也一樣,由于 {}
是一個空對象纸兔,所以除了 null
和 undefined
之外的基礎(chǔ)類型都可以視作繼承于 {}
派生出來的惰瓜。或者說如果一個值不是 null
和 undefined
就等于 這個值 & {}
的結(jié)果汉矿,如下
type T1 = 'a' & {}; // 'a'
type T2 = number & {}; // number
type T3 = object & {}; // object
type T4 = { a: string } & {}; // { a: string }
type T5 = null & {}; // never
type T6 = undefined & {}; // never
如果 T & {}
中的 T
不是 null/undefined
就可以認(rèn)為它肯定符合 {}
類型崎坊,就可以把 {}
從交叉類型中去掉了,如果是洲拇,則會被判為 never
奈揍,而 never
是會被忽略的(上面 Exclude
源碼定義里有提到),所以在結(jié)果里自然就排除掉了 null
和 undefined
赋续。
還有如果 T & {}
中的 T
是聯(lián)合類型男翰,是會觸發(fā)分發(fā)的,這個就不再解釋了
練習(xí)三
請實現(xiàn)一個能去掉對象類型中 null
和 undefined
的類型
// 需要把如下類型變成 { name: string }
type User = {
name: string
age: null,
gender: undefined
}
// 實現(xiàn)如下
type ObjNonNullable<T> = { [P in keyof T as T[P] extends null | undefined ? never : P]: T[P] };
type T1 = ObjNonNullable<User>
// type T1 = { name: string }
這里出現(xiàn)了一個本文第一次出現(xiàn)的關(guān)鍵字 as
蚕捉,我們知道它可以用來斷言奏篙,在 ts 4.1
版本可以在映射類型里用 as
實現(xiàn)鍵名重新映射,達(dá)到過濾或者修改屬性名的目的迫淹,如果指定的類型解析為 never
時秘通,會被忽略不會生成這個屬性
如上只能過濾對象第一層的 null
和 undefined
如何更進(jìn)一步改成可以遞歸的呢?
type User = {
name: string
age: undefined,
children: {
boy: number
girl: number
neutral: null
}
}
// 遞歸處理對象類型的 DeepNonNullable
type DeepNonNullable<T> = T extends object ? { [P in keyof T as T[P] extends null | undefined ? never : P]: DeepNonNullable<T[P]> } : T;
type T1 = DeepNonNullable<User>
// type T1 = {
// name: string;
// children: {
// boy: number;
// girl: number;
// };
//}
Awaited
Awaited<T>
:作用是獲取 async/await
函數(shù)或 promise
的 then()
方法的返回值的類型敛熬。而且自帶遞歸效果肺稀,如果是這樣嵌套的異步方法,也能拿到最終的返回值類型
示例:
// Promise
type T1 = Awaited<Promise<string>>;
// type T1 = string
// 嵌套 Promise应民,會遞歸
type T2 = Awaited<Promise<Promise<number>>>;
// type T2 = number
// 聯(lián)合類型话原,會觸發(fā)分發(fā)
type T3 = Awaited<boolean | Promise<number>>;
// type T3 = number | boolean
來看下源碼定義,看下到底是怎么執(zhí)行的诲锹,是怎么拿到結(jié)果的呢繁仁?
// 源碼定義
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F): any }
? F extends (value: infer V, ...args: any) => any
? Awaited<V>
: never
: T
泛型條件有點多,就換了下行归园,方便看
- 如果
T
是null
或undefined
就直接返回T
- 如果
T
是對象類型黄虱,并且里面有then
方法,就用infer
類型推斷出then
方法的第一個參數(shù)onfulfilled
的類型賦值給F
庸诱,onfulfilled
其實就是我們熟悉的resolve
捻浦。所以這里可以看出或者準(zhǔn)確的說,Awaited
拿的不是then()
的返回值類型桥爽,而是resolve()
的返回值類型- 既然
F
是回調(diào)函數(shù)resolve
朱灿,就推斷出該函數(shù)第一個參數(shù)類型賦值給V
,resolve
的參數(shù)自然就是返回值- 傳入
V
遞歸調(diào)用
- 傳入
-
F
不是函數(shù)就返回never
- 既然
- 如果
T
不是對象類型 或者 是對象但沒有then
方法钠四,返回T
盗扒,就是最后一行的T
Parameters
Parameters<T>
:作用是獲取函數(shù)所有參數(shù)的類型集合,返回的是元組。T
自然就是函數(shù)了
使用示例:
declare function f1(arg: { a: number; b: string }): void;
// 沒有參數(shù)的函數(shù)
type T1 = Parameters<() => string>;
// type T1 = []
// 一個參數(shù)的函數(shù)
type T2 = Parameters<(s: string) => void>;
// type T2 = [s: string]
// 泛型參數(shù)的函數(shù)
type T3 = Parameters<<T>(arg: T) => T>;
// type T3 = [arg: unknown]
// typeof f1 結(jié)果為 (arg: { a: number; b: string }) => void
type T4 = Parameters<typeof f1>;
// type T4 = [arg: {
// a: number;
// b: string;
// }]
// any 和 never
type T5 = Parameters<any>;
// type T5 = unknown[]
type T6 = Parameters<never>;
// type T6 = never
// 下面這樣傳參是會報錯的
type T7 = Parameters<string>;
type T8 = Parameters<Function>;
// 源碼定義
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
可以看到限制了函數(shù)類型侣灶,然后 ...args
取參數(shù)和 js
中的用法是一樣的习霹,infer
表示待推斷的類型變量,打斷出 ...args
取到的類型賦值給 P
ReturnType
ReturnType<T>
:作用是獲取函數(shù)返回值的類型炫隶。T
為函數(shù)
示例:
declare function f1(): { a: number; b: string };
type T1 = ReturnType<() => string>;
// type T1 = string
type T2 = ReturnType<(s: string) => void>;
// type T2 = void
type T3 = ReturnType<<T>() => T>;
// type T3 = unknown
type T4 = ReturnType<<T extends U, U extends number[]>() => T>;
// type T4 = number[]
type T5 = ReturnType<typeof f1>;
// type T5 = {
// a: number;
// b: string;
// }
// any 和 never
type T6 = ReturnType<any>;
// type T6 = any
type T7 = ReturnType<never>;
// type T7 = never
// 下面這樣是不行的
type T8 = ReturnType<string>;
type T9 = ReturnType<Function>;
// 源碼定義
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
可以看到源碼定義上和 Parameters
是基本一樣的淋叶,只是把類型推斷的參數(shù)換成返回值了
ConstructorParameters/InstanceType
我們知道 Parameters
和 ReturnType
這一對是獲取普通/箭頭函數(shù)的參數(shù)類型集合以及返回值類型的了,還有一對組合ConstructorParameters
和 InstanceType
是獲取構(gòu)造函數(shù)的參數(shù)類型集合以及返回值類型的伪阶,和上面的比較類似我就放到一起了
Uppercase/Lowercase
這倆兒的作用是轉(zhuǎn)換全部字母大小寫
type T1 = Uppercase<"abcd">
// type T1 = "ABCD"
type T2 = Lowercase<"ABCD">
// type T2 = "abcd"
Capitalize/Uncapitalize
這倆兒的作用是轉(zhuǎn)換首字母大小寫
type T1 = Capitalize<"abcd efg">
// type T1 = "Abcd efg"
type T2 = Uncapitalize<"ABCD EFG">
// type T2 = "aBCD EFG"
練習(xí)四
請實現(xiàn)一個類型煞檩,把對象類型中的屬性名換成大寫,需要注意的是對象屬性名支持 string | number | symbol
三種類型
type User1 = {
name: string
age: number
18: number
}
// 實現(xiàn)如下栅贴,只需調(diào)用現(xiàn)在的工具類型 Uppercase 就行了
// 先取出所有字符串屬性的出來斟湃,再處理返回 { NAME: string, AGE: number }
// type T1<T> = { [P in keyof T & string as Uppercase<P>]: T[P] }
// 只處理字符串屬性的,其他正常返回
type T1<T> = { [P in keyof T as P extends string ? Uppercase<P> : P]: T[P] }
type T2 = T1<User1>
// type T2 = {
// NAME: string;
// AGE: number;
// 18: number
// }
綜合練習(xí)
請實現(xiàn)一個類型檐薯,可以把下劃線屬性名的對象凝赛,換成駝峰屬性名的對象。這個就沒有現(xiàn)成的工具類型調(diào)用了坛缕,所以需要我們額外實現(xiàn)一個
這個練習(xí)用到了本文中的很多知識墓猎,先自己寫一下咯
type User1 = {
my_name: string
my_age_type: number // 多個下劃線
my_children: {
my_boy: number
my_girl: number
}
}
// 實現(xiàn)如下
type T1<T> = T extends string
? T extends `${infer A}_${infer B}`
? `${A}${T1<Capitalize<B>>}` // 這里有遞歸處理單個屬性名多個下劃線
: T
: T;
// 對象不遞歸
// type T2<T> = { [P in keyof T as T1<P>]: T[P] }
// 對象遞歸
type T2<T> = T extends object ? { [P in keyof T as T1<P>]: T2<T[P]> } : T
type T3 = T2<User1>
// type T3 = {
// myName: string;
// myAgeType: number;
// myChildren: {
// myBoy: number;
// myGirl: number;
// };
// }
這個練習(xí)用到了 extends
、infer
赚楚、as
毙沾、循環(huán)
、遞歸
宠页,相信能更好地幫助我們理解和運用
結(jié)語
如果本文對你有一點點幫助左胞,點個贊支持一下吧,你的每一個【贊】都是我創(chuàng)作的最大動力 _
更多前端文章举户,或者加入前端交流群烤宙,歡迎關(guān)注公眾號【前端快樂多】,大家一起共同交流和進(jìn)步呀
參考資料
https://www.typescriptlang.org/docs/handbook/utility-types.html