[Day08] TS:什麼!型別也能做條件判斷?認識 Conditional Types
上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!
昨天我們對於 Utility Types 有了更多認識,發現原來 Utility Types 可以像函式一樣接受型別當作 input 然後回傳一個新的型別當作 output。今天來看一下原來在 TypeScript 中,型別還可以做到「條件判斷(Conditional Types)」!?
在 TypeScript 中,雖然沒辦法針對型別使用 if ... else
來做條件判斷,但可以使用 JavaScript 中同樣有的三元運算子(ternary operator)來達到條件判斷的目的,也就是用 ?
和 :
。
語法大概會像這樣:
這裡讀者可以留意到 X extends Y
就像是一個放在 if()
括號內的條件式,這裡的 extends
就像先前在泛型時提到過的,要當作是「能夠滿足」的意思。X 能夠滿足 Y,更確切來說是指「X 是 Y 的子集合(subset)」。
在學習 Conditional Types 或 Utility Types 時,可以多利用 Type Alias 的方式,建立一個型別後,看看它最終會是什麼。以 Conditional Type 來說,可以用它來建立一個名為 NewType
型別:
type NewType = X extends Y ? true : false;
上面的意思翻成白話文就是「如果 X 是 Y 的子集合的話,則 NewType 就會是 true,否則的話 NewType 會是 false」。
關於 extends
的使用我喜歡用「集合」搭配圖像的方式來思考,X extends Y
表示 Y
是比 X
更大的集合:
如果你覺得還有點太過抽象,可以把抽象的東西用一些具體的值帶入,可以幫助我們更好了解。
現在再把它更具體一點:
- 定義
SomeType1
,讓它的值直接是個字串(String Literal Types) - 使用 conditional type 建立
NewType1
type SomeType1 = 'any-string'; // String Literal Types
type NewType1 = SomeType1 extends string ? true : false; // true
這時因為 SomeType1
是 String Literal Types,它是 string
的子集合,所以 NewType1
就會是 true
。用圖像來描述的話會像是這樣:
接著定義另一個 SomeType2
,讓它的值直接是個數字(Number Literal Types):
type SomeType2 = 0; // Number Literal Types
type NewType2 = SomeType2 extends string ? true : false; // false
這時候因為 SomeType2 是 Number Literal Types,並不是 string
的子集合,因此 NewType2
就會是 :
後的 false
。用圖像來表示的話會是這樣:
這就是 Conditional Types 的語法。Conditional Type 雖然看起來簡單,但實際上可以做出很多不同的變化,我們會在後面幾天一直看到它的身影。
在 TypeScript 中,可以直接使用「布林值」、「字串值」和「數值的值」作為型別,這種用法稱作 Literal Types。
實際範例
在了解 Conditional Types 的使用後,讓我們來看官網提供的 Flatten 這個範例, Flatten
一樣可以視為一個 Utility Type:
讓我們先來理解一下 T extends any[]
是什麼意思。
除了把抽象的東西用一些具體的值帶入之外,在理解 Conditional Types 時,可以把 ?
和 :
後的內容先替換成其他內容,方便理解它是跑到了那一個條件。
這裡先把 ?
後的值改成 true
,:
後的值改成 false
,像是這樣:
// 當我們不清楚「條件」是什麼意思是,可以修改 ? 和 : 後的內容
type Flatten<T> = T extends any[] ? true : false;
接著分別定義型別 Manufacture
和 Manufactures
,然後把它帶 入 Flatten
的 <>
中:
type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';
type Manufactures = Manufacture[];
type FlattenManufacture = Flatten<Manufacture>; // false
type FlattenManufactures = Flatten<Manufactures>; // true
把滑鼠移到 FlattenManufacture
或 FlattenManufactures
就可以看到回傳的型別:
你會發現如果帶入的是 Manufacture
,因為 Manufacture
不是 any[]
的子集合,因此會進到 false
:
相反地,如果帶入的是 Manufactures
,以為 Manufactures
本身就是 Manufacture[]
,可以算是 any[]
的子集合,進而得到 true
:
搭配原本對於 TypeScript 的理解,應該就可以知道 T extends any[]
翻成白話文,指的就是「T 是否符合陣列型別」。
在理解 Condition 後,接著讓我們把 Flatten
改回原本的樣子:
type Flatten<T> = T extends any[] ? T[number] : T;
現在我們可以理解,當 T
不是陣列型別的子集合時,它會直接回傳 :
後的 T
,也就是什麼都不做直接回傳原本的型別。但如果 T
是陣列型別的子集合,那 T[number]
是什麼意思呢?
同樣的,我們可以帶一個實際的型別來取代 T
幫助我們理解。我們把 T
用符合陣列型別的 Manufactures
帶進去看看:
type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';
type Manufactures = Manufacture[];
type ShowMeTheType = Manufactures[number];
接著把滑鼠移到 ShowMeTheType
上面:
你會發現,因為 Manufactures
的型別是 Manufacture[]
,而使用 Manufactures[number]
的意思其實就是把陣列型別裡的元素型別取出的意思(可以參考內文後方針對 Index Access Types 的補充)。
回到一開始的範例 ,可以知道當 T
是陣列型別的子集合時,會得到的是 T[number]
,也就是取出 T
這個陣列型別中元素的型別。所以說 Flatten
這個 Utility Type 的作用就是「把陣列型別中的元素型別攤平後回傳,如果不是陣列元素就什麼都不要做」的意思。
使用上可以像這樣:
type FlattenManufacture = Flatten<Manufacture>; // "Apple" | "Google" | "Samsung" | "Sony"
type FlattenManufactures = Flatten<Manufacture[]>; // "Apple" | "Google" | "Samsung" | "Sony"
type Example1 = Flatten<['a', true, 3]>; // true | 3 | "a"
type Example2 = Flatten<string[]>; // string
type Example3 = Flatten<'not array'>; // "not array"
補充:使用 Indexed Access Types 取得陣列型別中元素的型別
稍微補充一下,在 Day 05 的時候我們有提了 Indexed Access Types,當時提到的是針對物件型別來使用 Indexed Access Type 的話,可以取出物件型別中屬性值的型別,但如是針對陣列型別的話,這可以使用 Indexed Access Types 來取出陣列型別中元素型別,因為陣列型別的 index 一定是 number
型別的緣故,所以只要使用 SomeArray[number]
就可以取出 SomeArray
這個陣列型別裡面元素值的型別。
舉例來說:
type StringArray = string[];
type StringArrayElement = StringArray[number]; // string
type NumberArray = number[];
type NumberArrayElement = NumberArray[number]; // number
當然也可以用實際數值的方式取出某 index 元素的型別:
type SomeArray = [string, boolean, number];
type Element0 = SomeArray[0]; // string
type Element1 = SomeArray[1]; // boolean
type Element2 = SomeArray[2]; // number
type Elements = SomeArray[number]; // string | boolean | number
Conditional Types 學習小技巧
- 在理解 Conditional Types 時,可以把
?
和:
後的內容先替換成其他內容,方便理解它是跑 到了那一個條件。這招非常實用,特別是在未來你可以能會看到多重的? A : B ? C : D
時。 - 所謂「X 滿足 Y」的意思,確切來說指的是「X 是 Y 的子集合(subset)」。
範例程式碼
https://tsplay.dev/wXkB8W @ TypeScript Playground
參考資料
- Conditional Types @ TypeScript > Type Manipulation
- Literal Types @ Typescript > Everyday Types