Parameters
Parameters 用於提取函式型別的引數型別。
原始碼是這樣的:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
1. 引數說明
T
:要從中提取引數型別的函式型別。
2. 條件型別 T extends (...args: infer P) => any
這個條件型別用於檢查
T
是否是一個函式型別。(...args: infer P) => any
是一個函式簽名,其中...args: infer P
表示函式可以接受任意數量和型別的引數,infer P
用於推斷函式的引數型別。
3. 分支 ? P : never
如果
T
是一個函式型別,並且成功推斷出引數型別P
,則返回P
。如果
T
不是一個函式型別,或者無法推斷出引數型別,則返回never
。
型別引數 T 為待處理的型別,透過 extends 約束為函式,引數和返回值任意。
透過 extends 匹配一個模式型別,提取引數的型別到 infer 宣告的區域性變數 P 中返回。
這樣就實現了函式引數型別的提取:
function add(a: number, b: number): number { return a + b; } type AddParameters = Parameters<typeof add>; // [number, number]
ReturnType
ReturnType 用於提取函式型別的返回值型別。
原始碼是這樣的:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
1. 引數說明
T
:要從中提取返回型別的函式型別。
2. 條件型別 T extends (...args: any) => infer R
這個條件型別用於檢查
T
是否是一個函式型別。(...args: any) => infer R
是一個函式簽名,其中...args: any
表示函式可以接受任意數量和型別的引數,infer R
用於推斷函式的返回型別。
3. 分支 ? R : any
如果
T
是一個函式型別,並且成功推斷出返回型別R
,則返回R
。如果
T
不是一個函式型別,或者無法推斷出返回型別,則返回any
。
型別引數 T 為待處理的型別,透過 extends 約束為函式型別,引數和返回值任意。
用 T 匹配一個模式型別,提取返回值的型別到 infer 宣告的區域性變數 R 裡返回。
這樣就實現了函式返回值型別的提取:
function add(a: number, b: number): number { return a + b; } type AddReturnType = ReturnType<typeof add>; // number
typeof add
是function (a: number, b: number): number
。ReturnType<typeof add>
成功推斷出返回型別number
。
Partial
索引型別可以透過對映型別的語法做修改,比如把索引變為可選。
type Partial<T> = { [P in keyof T]?: T[P]; };
1. 引數說明
T
:要處理的物件型別。
2. 對映型別 [P in keyof T]
keyof T
獲取T
型別的所有鍵的聯合型別。[P in keyof T]
是一個對映型別,遍歷T
的所有鍵P
。
3. 可選屬性 ?
T[P]?
表示將T
的每個屬性P
變為可選屬性。
型別引數 T 為待處理的型別。
透過對映型別的語法構造一個新的索引型別返回,索引 P 是來源於之前的 T 型別的索引,也就是 P in keyof T,索引值的型別也是之前的,也就是 T[P]。
這樣就實現了把索引型別的索引變為可選的效果:
interface User { id: number; profile: { name: string; email: string; }; } type PartialUser = Partial<User>;
User
型別的屬性id
和profile
都是必需的。Partial<User>
將這些屬性變為可選的,因此PartialUser
的型別是:{ id?: number; profile?: { name: string; email: string; }; }
Required
可以把索引變為可選,也同樣可以去掉可選,也就是 Required 型別:
type Required<T> = { [P in keyof T]-?: T[P]; };
1. 引數說明
T
:要處理的物件型別。
2. 對映型別 [P in keyof T]-?
keyof T
獲取T
型別的所有鍵的聯合型別。[P in keyof T]-?
是一個對映型別,遍歷T
的所有鍵P
,並將每個屬性P
的可選性移除(即變為必需)。
3. 必需屬性 -?
T[P]-?
表示將T
的每個屬性P
的可選性移除,使其成為必需屬性。
型別引數 T 為待處理的型別。
透過對映型別的語法構造一個新的索引型別,索引取自之前的索引,也就是 P in keyof T,但是要去掉可選,也就是 -?,值的型別也是之前的,就是 T[P]。
這樣就實現了去掉可選修飾的目的:
interface Person { name?: string; age?: number; address: string; } type RequiredPerson = Required<Person>;
Person
型別的屬性name
和age
是可選的,而address
是必需的。Required<Person>
將所有屬性變為必需的,因此RequiredPerson
的型別是:{ name: string; age: number; address: string; }
Readonly
同樣的方式,也可以新增 readonly 的修飾:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
型別引數 T 為待處理的型別。
透過對映型別的語法構造一個新的索引型別返回,索引和值的型別都是之前的,也就是 P in keyof T 和 T[P],但是要加上 readonly 的修飾。
Pick
對映型別的語法用於構造新的索引型別,在構造的過程中可以對索引和值做一些修改或過濾。
比如可以用 Pick 實現過濾:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
1. 引數說明
T
:要從中選擇屬性的物件型別。K
:要選擇的屬性鍵的聯合型別,必須是T
的鍵的子集(即K
必須是keyof T
的一部分)。
2. 對映型別 [P in K]
keyof T
獲取T
型別的所有鍵的聯合型別。[P in K]
是一個對映型別,遍歷K
中的所有鍵P
。
3. 屬性對映 T[P]
T[P]
表示從T
型別中選擇鍵P
對應的屬性型別。
型別引數 T 為待處理的型別,型別引數 K 為要過濾出的索引,透過 extends 約束為只能是 T 的索引的子集。
構造新的索引型別返回,索引取自 K,也就是 P in K,值則是它對應的原來的值,也就是 T[P]。
這樣就實現了過濾的目的:
interface Person { name: string; age: number; address: string; } type PersonNameAndAge = Pick<Person, 'name' | 'age'>;
Person
型別的屬性name
、age
和address
都是必需的。Pick<Person, 'name' | 'age'>
選擇name
和age
屬性,因此PersonNameAndAge
的型別是:{ name: string; age: number; }
Record
Record 用於建立索引型別,傳入 key 和值的型別:
type Record<K extends keyof any, T> = { [P in K]: T; };
1. 引數說明
K
:鍵的聯合型別,必須是keyof any
的子集,即K
必須是有效的鍵名。T
:每個鍵對應的值型別。
2. 對映型別 [P in K]
[P in K]
是一個對映型別,遍歷K
中的所有鍵P
。
3. 屬性對映 T
T
表示每個鍵P
對應的值型別。
type UserStatus = 'active' | 'inactive' | 'pending'; type UserStatusCount = Record<UserStatus, number>; const userStatusCount: UserStatusCount = { active: 10, inactive: 5, pending: 3 };
UserStatusCount
型別的鍵是'active'
、'inactive'
和'pending'
,每個鍵的值型別是number
。因此,
userStatusCount
變數必須包含這三個鍵,並且每個鍵的值必須是數字。
Exclude
當想從一個聯合型別中去掉一部分型別時,可以用 Exclude 型別:
type Exclude<T, U> = T extends U ? never : T;
1. 引數說明
T
:源型別,從中排除某些部分。U
:要排除的型別。
2. 條件型別 T extends U ? never : T
這個條件型別用於檢查
T
是否是U
的子型別。如果
T
是U
的子型別,則返回never
,表示從T
中排除這部分。如果
T
不是U
的子型別,則返回T
,表示保留這部分。
聯合型別當作為型別引數出現在條件型別左邊時,會被分散成單個型別傳入,這叫做分散式條件型別。
所以寫法上可以簡化, T extends U 就是對每個型別的判斷。
過濾掉 U 型別,剩下的型別組成聯合型別。也就是取差集。
type AllTypes = string | number | boolean | null | undefined; type ExcludeNullish = Exclude<AllTypes, null | undefined>; // ExcludeNullish 的型別是 string | number | boolean
AllTypes
是一個聯合型別,包含string
、number
、boolean
、null
和undefined
。Exclude<AllTypes, null | undefined>
排除了null
和undefined
,因此ExcludeNullish
的型別是string | number | boolean
。
Extract
可以過濾掉,自然也可以保留,Exclude 反過來就是 Extract,也就是取交集:
type Extract<T, U> = T extends U ? T : never;
1. 引數說明
T
:源型別,從中提取某些部分。U
:要提取的型別。
2. 條件型別 T extends U ? T : never
這個條件型別用於檢查
T
是否是U
的子型別。如果
T
是U
的子型別,則返回T
,表示保留這部分。如果
T
不是U
的子型別,則返回never
,表示排除這部分。
示例
基本示例
type AllTypes = string | number | boolean | null | undefined; type ExtractStrings = Extract<AllTypes, string>; // ExtractStrings 的型別是 string
AllTypes
是一個聯合型別,包含string
、number
、boolean
、null
和undefined
。Extract<AllTypes, string>
提取了string
型別,因此ExtractStrings
的型別是string
。
Omit
我們知道了 Pick 可以取出索引型別的一部分索引構造成新的索引型別,那反過來就是去掉這部分索引構造成新的索引型別。
可以結合 Exclude 來輕鬆實現:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
1. 引數說明
T
:要從中排除屬性的物件型別。K
:要排除的屬性鍵的聯合型別,必須是keyof any
的子集,即K
必須是有效的鍵名。
2. 組合使用 Pick
和 Exclude
keyof T
獲取T
型別的所有鍵的聯合型別。Exclude<keyof T, K>
從keyof T
中排除K
中的鍵。Pick<T, Exclude<keyof T, K>>
從T
型別中選擇排除後的鍵。
型別引數 T 為待處理的型別,型別引數 K 為索引允許的型別(string | number | symbol 或者 string)。
透過 Pick 取出一部分索引構造成新的索引型別,這裏用 Exclude 把 K 對應的索引去掉,把剩下的索引保留。
這樣就實現了刪除一部分索引的目的:
interface Person { name: string; age: number; address: string; email: string; } type PersonWithoutEmail = Omit<Person, 'email'>; const person: PersonWithoutEmail = { name: 'John Doe', age: 30, address: '123 Main St' };
Person
型別的屬性name
、age
、address
和email
都是必需的。Omit<Person, 'email'>
排除了email
屬性,因此PersonWithoutEmail
的型別是:{ name: string; age: number; address: string; }
Awaited
在遞迴那節我們寫過取 Promise 的 ValuType 的高階型別,這個比較常用,ts 也給內建了,就是 Awaited。
它的實現比我們當時寫的完善一些:
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;
1. 引數說明
T
:要從中提取最終值的型別。
2. 條件型別 T extends null | undefined
檢查
T
是否是null
或undefined
。如果是,則直接返回
T
。
3. 條件型別 T extends object & { then(onfulfilled: infer F): any }
檢查
T
是否是一個物件,並且具有then
方法。then
方法的引數型別被推斷為F
。
4. 條件型別 F extends ((value: infer V, ...args: any) => any)
檢查
F
是否是一個函式,並且其第一個引數型別被推斷為V
。
5. 遞迴呼叫 Awaited<V>
如果
F
是一個函式,並且其第一個引數型別為V
,則遞迴呼叫Awaited<V>
,繼續解析V
。
6. 預設返回 T
如果
T
不是null
或undefined
,且不具有then
方法,則直接返回T
。
型別引數 T 是待處理的型別。
如果 T 是 null 或者 undefined,就返回 T。
如果 T 是物件並且有 then 方法,那就提取 then 的引數,也就是 onfulfilled 函式的型別到 infer 宣告的區域性變數 F。
繼續提取 onfullfilled 函式型別的第一個引數的型別,也就是 Promise 返回的值的型別到 infer 宣告的區域性變數 V。
遞迴的處理提取出來的 V,直到不再滿足上面的條件。
這樣就實現了取出巢狀 Promise 的值的型別的目的:
type Result2 = Awaited<Promise<Promise<number>>>; // number
NonNullable
NonNullable 就是用於判斷是否為非空型別,也就是不是 null 或者 undefined 的型別的,實現比較簡單:
type NonNullable<T> = T extends null | undefined ? never : T;
1. 引數說明
T
:要從中排除null
和undefined
的型別。
2. 條件型別 T extends null | undefined ? never : T
這個條件型別用於檢查
T
是否是null
或undefined
。如果
T
是null
或undefined
,則返回never
,表示從T
中排除這部分。如果
T
不是null
或undefined
,則返回T
,表示保留這部分。
示例
基本示例
type NullableTypes = string | number | null | undefined; type NonNullableTypes = NonNullable<NullableTypes>; // NonNullableTypes 的型別是 string | number
NullableTypes
是一個聯合型別,包含string
、number
、null
和undefined
。NonNullable<NullableTypes>
排除了null
和undefined
,因此NonNullableTypes
的型別是string | number
。
Uppercase、Lowercase、Capitalize、Uncapitalize
這四個型別是分別實現大寫、小寫、首字母大寫、去掉首字母大寫的。
它們的原始碼時這樣的:
type Uppercase<S extends string> = intrinsic; type Lowercase<S extends string> = intrinsic; type Capitalize<S extends string> = intrinsic; type Uncapitalize<S extends string> = intrinsic;
啥情況,intrinsic 是啥?
這個 intrinsic 是固有的意思,就像 js 裡面的有的方法列印會顯示 [native code] 一樣。這部分型別不是在 ts 裏實現的,而是編譯過程中由 js 實現的。
其實就是 ts 編譯器處理到這幾個型別時就直接用 js 給算出來了。
為啥要這樣做呢?
因為快啊,解析型別是要處理 AST 的,效能比較差,用 js 直接給算出來那多快呀。
總結
雖然我們學完 6 個套路,各種型別程式設計邏輯都能寫了,但是常用的型別 TS 已經內建了。
這些內建的高階型別用我們學的套路很容易可以實現。
比如用模式匹配可以實現:Parameters、ReturnType
用模式匹配 + 重新構造可以實現:OmitThisParameter
用重新構造可以實現:Partial、Required、Readonly、Pick、Record
用模式匹配 + 遞迴可以實現: Awaited
用聯合型別在分散式條件型別的特性可以實現: Exclude
此外還有 NonNullable 和四個編譯器內部實現的型別:Uppercase、Lowercase、Capitalize、Uncapitalize。
這些型別也不咋需要記,就算忘記了自己也能很快的實現。重點還是放在 6 個型別程式設計的套路上。