跳至主要内容

[Day10] TS:什麼!Conditional Types 中還能建立型別?使用 infer 來實作 ReturnType 和 Parameters

carbon

今天會來說明 TypeScript 中內建 ReturnTypeParameters 的原始碼(像是上圖這樣),如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!

ReturnType 和 Parameters 的使用

一樣讓我們先來簡單了解 ReturnTypeParameters 這兩個 Utility Types 的使用。

ReturnType

ReturnType<T> 是 TypeScript 內建的 Utility Type,它能夠接受一個參數 T,這個參數滿足「函式型別」的話(即,T 要是函式型別的子集合),則會回傳這個函式「回傳值的型別(return type)」;否則,就會回傳 any

來看一下官網提供的幾個範例:

type T1 = ReturnType<() => string>; // string

type T2 = ReturnType<(s: string) => number[]>; // number[]
  • 第一個範例中,ReturnType<T><> 中帶入的是 () => string 這個函式型別,因為這個函式會回傳 string,所以 T1 會是 string
  • 第二個範例中,因為 (s: string) => number[] 會回傳 number[],所以 T1 會是 number[]

如果帶入的型別不符合,則會回傳 any

type T3 = ReturnType<string>; // any

有個稍微特別的地方是,雖然 T3 的型別會是 any,但 TypeScript 會在 string 的地方跳出錯誤提示,至於為什麼會這樣,等等看原始碼的時候就會瞭解了!

ReturnType

Parameters

Parameters<T> 則是 TypeScript 內建的另一個 Utility Type,它能夠接受一個參數 T,這個參數滿足「函式型別」的話(即,T 要是函式型別的子集合),則會以「 tuple type 來回傳函式的「參數(parameters)」,否則會回傳 never

一樣來看看幾個例子,這四個範例帶進去 T 的型別都能滿足函式型別:

type T1 = Parameters<(a: number, b: string) => number>; // [a: number, b: string]

type T2 = Parameters<(a: number[]) => number>; // [a: number[]]

type T3 = Parameters<(a: { firstName: string; lastName: string }) => string>; // [a: { firstName: string; lastName: string; }]

type T4 = Parameters<(...a: number[]) => number>; // number[]
  • 在第一個範例中,因為帶入 <T> 內的 (a: number, b: string) => number 是函式型別的子集合,所以 Parameters 這個 Utility Type 就會把函式的參數用 tuple type 的方式回傳出來,因此 T1 會是 [a: number, b: string]
  • 第二和第三個範例也是一樣的意思,只是分別帶入的參數型別是陣列和物件而已。
  • 第四個範例比較特別一點,讀者需要先了解 JavaScript 中可以使用 rest parameters 來取得所有函式參數的內容,如此就可以理解為什麼它 T3 會是 number[] 而不是 [number[]] 了。

如果帶入 <T> 的型別不是函式型別的子集合的話,則會得到 never

type T5 = Parameters<string>; // never

同樣的,雖然有得到 T5 的型別是 never,但 TypeScript 會在 string 的地方跳出錯誤提示,一樣等等看原始碼時就會知道為什麼:

Parameters

認識 Conditional Types 中的 infer

如果直接看 ReturnType 這個 Utility Type 的原始碼時,讀者會看到一個先前沒提到過的關鍵字 — infer

infer

勢必要先了解 infer 才能理解 ReturnType 的原始碼,所以就先來看看這個 infer 怎麼用吧!

在前幾天講 Conditional Types 時筆者曾經提到 X extends Y ? T : F 中的 X extends Y 指的是「當 X 是 Y 的子集合」。但如果現在的 Y 並不是一個確切的型別,我們想要讓 TypeScript 幫我們推導其型別的話,就可以用 infer 這個關鍵字。

寫起來會像這樣:

infer

infer R 中的這個 R 就是 TypeScript 自己推導出來的型別,而且它是可以當 Conditional Type 的條件為 true 時,這個 R 是可以直接被拿來當成回傳值使用的。

因為 infer 的概念比較抽象,透過實際範例會比較好理解,來我們先來看幾個例子。

範例一

前幾天在說明 Conditional Types 時,曾使用 Flatten 來做示範:

Conditional Types

現在我們把 any[] 的部分修改成 (infer R)[],也就是 any 變成 infer R(加上括號是為了讓 TS 在解析語法時不會混淆):

infer

這時候 TS 就會根據使用者帶入的型別,自動推導這個 R 應該是什麼型別。同時,被推導出來的 R 還可以在條件為 true 時作為回傳值使用。

讀者可以猜想下面的 R 會是什麼呢?

type Flatten<T> = T extends (infer R)[] ? R : T;

type T1 = Flatten<number[]>; // number,且 R 會是 number

type T2 = Flatten<(string | number)[]>; // string | number,且 R 會是 string | number

type T3 = Flatten<number>; // number

如果 T 是陣列型別的子集合的話,則會回傳這個被推導出來的 R,否則直接回傳 T

  • 第一個例子中,因為帶入 <> 的是 number[],所以 R 會被推論是 number,因此 T1 就會是 number
  • 第二個例子中,因為帶入 <> 的是 (string | number)[],所以 R 會被推論是 string | number,因此 T2 就會是 string | number
  • 第三個例子中,因為帶入 <> 的是 number,而 number 並不是陣列型別的子集合,所以會直接回傳原本帶入 <> 的型別。

範例二

讓我們再看另一個例子:

type InferResp<T> = T extends { response: infer R; status: number } ? R : T;

在這個 Utility Type 中,T 如果是 { response: ...; status: number } 的子集合,則會回傳 R,否則會回傳 T,但這裡並不清楚 response 的型別是什麼,所以使用 { response: infer R, ... } 來讓 TS 推論:

type T1 = InferResp<{ response: { data: 'foobar' }; status: 200 }>; // { data: 'foobar' }

type T2 = InferResp<{ status: 400 }>; // { status: 400 }

在上面的例子中可以看到,當帶入的 T 滿足 { response: ...; status: number } 時,R 就可以自動被推導成 {data: 'foobar'}。但如果不滿足的話,就會直接回傳原本帶入的型別 { status: 400 }

從上面的例子中可以看到,infer 適合用在需要做條件判斷,但型別又不完全明確時使用

這個 infer 很特別,需要多感覺一下,等等說明 ReturnType 的原始碼時可以再體會看看。有幾個使用 infer 時一定要留意的細節:

  • 關鍵字 infer 只能在 Conditional Types 中的 extends 被使用(更確切來說是 extends 後且 ? 前),不能在限制泛型(Generics Constraint)中的 extends 使用。
  • 使用 infer R 後,這個被推導出來的型別 R 雖然能夠被當成型別直接回傳,但它只能用在符合 True 的條件使用(即,? 後且 : 前),不能用在 False 的情況(即,: 後)

在 infer 中使用 constraint

extends Constraints on infer Type Variables @ microsoft devblogs

在 TypeScript 4.7 後,可以在 infer 後使用 extends,例如:

// 在 TypeScript 4.7 以後可以這樣寫
type FirstIfString<T> =
// 在 infer 內使用 extends
T extends [infer S extends string, ...unknown[]]
? S
: never;

這個的意思是,inferS 這個 type,且這個 type 需要滿足 string 才行。如此可以避免 TypeScript 4.7 以前,需要使用 nested conditionals 的寫法:

// 在 TypeScript 4.7 以前需要這樣寫
type FirstIfString<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;

理解 ReturnType 和 Parameters 的實作

在認識 infer 之後,讓我們回頭來看 ReturnTypeParameters 這兩個 Utility Types 的原始碼。

要看懂這兩個 Utility Types 的原始碼,前面幾天提到的知識缺一不可:

ReturnType

ReturnType 的原始碼是:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R
? R
: any;

在看他人寫的 Utility Types 時,很重要的是做正確的斷句,斷好句後通常就會比較好理解它的意思,這裡我們來幫它斷句一下:

ReturnType

先看泛型的部分,也就是 <T extends (...args: any) => any>,這裡面同時有兩個 > 在內,一開始會讓人容易有點混淆,但讀者只要知道 (...args: any) => any 這個是 TypeScript 的 Function Type,意思就是這個函式可以接受任何型別作為參數,也可回傳任何型別的值。

根據前幾天對於 Generics Constraint 的說明,將可以理解 <T extends (...args: any) => any> 完整的意思就是說,T 需要是 (...args: any) => any 的子集合,也就是說 T 需要滿足函式型別,不論這個函式的參數和回傳值的型別是什麼都可以。

現在讀者應該可以知道,為什麼在剛剛的範例中使用 ReturnType<string> 時,TS 會回報錯誤提示了,這是因為 string 並不是函式型別的子集合。

接著把注意力放到等號後的 Conditional Types,T extends (...args: any) => infer R ? R : any;,根據前幾天對 Conditional Types 的說明,讀者應該可以知道,這裡的判斷式 T extends (...args: any) => infer R,如果這個判斷式為真,就會回傳 R,否則會回傳 any

看到這裡讀者應該可以理解,為什麼在剛剛的範例中使用 ReturnType<string> 時,雖然 TS 有報錯,但最終還是可以得到 any 的型別。因為如果帶入的 T 不滿足函式型別的話,就會得到 any

ReturnType

最後來看看當使用者帶入的 T 滿足函式型別時,回傳的 R 是什麼。這個 R 就和今天認識的 infer 有關,在 Conditional Types 中的 T extends (...args: any) => infer R,這裡用了 infer R 來推論這個函式會回傳的型別,並把這個函式會回傳的型別取名為 R,所以 R 指的就是函式會回傳的型別。

Parameters

最後來看看 Parameters<T>,它的原始碼是:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P
: never;

要看懂這段一樣需要先試著將它們斷句,讀者們可以先自己試著練習看看。

Parameters

斷句後我們可以看到,前面 Generic Constraints 的部分是 <T extends (...args: any) => any>,需要滿足的限制是 (...args: any) => any,這個部分和 ReturnType 是一樣的,也就是只要符合函式型別即可,不論該函式的參數或回傳值的型別是什麼。

在等號後的 Conditional Types 中可以看到如果 T 是函式型別的子集合(即,T extends (...args: infer P) => any),就會回傳 P,否則會回傳 never

那麼這個 P 是什麼呢?從 (...args: infer P) 可以看出這個 P 是透過 infer 來推論帶入 <> 中函式的「參數的型別」,推論後命名為 P,並作為回傳值使用。

範例程式碼

https://tsplay.dev/WYY9rW @ TypeScript Playground

infer 學習重點

  • 關鍵字 infer 只能用在 Conditional Types 的 extends 後與 ? 前,不能用在 Generics Constraint 的 extends
  • 透過 infer 推導出來的型別只能在 Conditional Types 中為 true 時被使用,不能在 false 是被使用
  • 如果未來碰到比較複雜的 Utility Types,不知道 infer 出來的型別是什麼,就回傳出來看看

參考資料