跳至主要内容

[Day08] TS:什麼!型別也能做條件判斷?認識 Conditional Types

Conditional Types

上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!

昨天我們對於 Utility Types 有了更多認識,發現原來 Utility Types 可以像函式一樣接受型別當作 input 然後回傳一個新的型別當作 output。今天來看一下原來在 TypeScript 中,型別還可以做到「條件判斷(Conditional Types)」!?

在 TypeScript 中,雖然沒辦法針對型別使用 if ... else 來做條件判斷,但可以使用 JavaScript 中同樣有的三元運算子(ternary operator)來達到條件判斷的目的,也就是用 ?:

語法大概會像這樣:

Conditional Types

這裡讀者可以留意到 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 更大的集合:

Conditional Types

如果你覺得還有點太過抽象,可以把抽象的東西用一些具體的值帶入,可以幫助我們更好了解。

現在再把它更具體一點:

  • 定義 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。用圖像來描述的話會像是這樣:

Conditional Types

接著定義另一個 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 Types 的語法。Conditional Type 雖然看起來簡單,但實際上可以做出很多不同的變化,我們會在後面幾天一直看到它的身影。

在 TypeScript 中,可以直接使用「布林值」、「字串值」和「數值的值」作為型別,這種用法稱作 Literal Types

實際範例

在了解 Conditional Types 的使用後,讓我們來看官網提供的 Flatten 這個範例, Flatten 一樣可以視為一個 Utility Type:

Conditional Types

讓我們先來理解一下 T extends any[] 是什麼意思。

除了把抽象的東西用一些具體的值帶入之外,在理解 Conditional Types 時,可以把 ?: 後的內容先替換成其他內容,方便理解它是跑到了那一個條件

這裡先把 ? 後的值改成 true: 後的值改成 false,像是這樣:

// 當我們不清楚「條件」是什麼意思是,可以修改 ? 和 : 後的內容
type Flatten<T> = T extends any[] ? true : false;

接著分別定義型別 ManufactureManufactures,然後把它帶入 Flatten<> 中:

type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';
type Manufactures = Manufacture[];

type FlattenManufacture = Flatten<Manufacture>; // false
type FlattenManufactures = Flatten<Manufactures>; // true

把滑鼠移到 FlattenManufactureFlattenManufactures 就可以看到回傳的型別:

conditional types

你會發現如果帶入的是 Manufacture ,因為 Manufacture 不是 any[] 的子集合,因此會進到 false

Conditional Types

相反地,如果帶入的是 Manufactures,以為 Manufactures 本身就是 Manufacture[],可以算是 any[] 的子集合,進而得到 true

Conditional Types

搭配原本對於 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 上面:

Indexed Access Types

你會發現,因為 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

參考資料