跳至主要内容

[Day07] TS:什麼是 Utility Types?

Utility Type

上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我同事 Andy 「前端工程師學習 DevOps 之路」的精彩文章!。

昨天我們整合了過去所學的知識寫了一個函式,後面我們會提到更多 TypeScript 中用來建立 Utility Types 所需的知識,但今天讓我們繼續熟悉前幾天學到的內容,並試著建立一些常用的 Utility Types 吧!

前幾天學到的:

  • 泛型(generics)的使用
  • 使用 extends 限制泛型
  • keyof 的使用
  • Indexed Access Types 的使用

Utility Types 是什麼

一般寫程式時,或多或少會寫過一些 utility function,它們就像小工具,可以接受 input 然後做了某些處理後回傳 output,舉例來說,以阿拉伯數字作為 input,接著以中文的數字作為 output;或者以字串作為 input,根據某些字元拆成陣列後作為 output。不管功能是什麼,這種「小工具」類,有 input 和 output 的函式,就可以稱作 utility。

除了函式之外,在 TypeScript 中,也有不少處理型別的小工具可以使用,和前面提到的 utility functions 最大的不同在於,代入 Utility Types 的 input 會是 TypeScript 的「型別」,而不是一般的 JavaScript value,也就是說,Utility Types 會以「型別」作為 input,並且以另一個「型別」作為 output,也就是說,Utility Types 就像函式一樣可以帶入 input 得到 output,透過 Utility Types 將可以「根據一個型別,來建立出另一個型別」

Utility Types 有時也稱作 Type Function,TypeScript 本身就有許多內建的 Utility Types,像是 PartialRequiredRecord、...等等。這裡我們先來看一下比較基本的,先對 Utility Types 有一點感覺,等後面學到更多知識後,再來看其他更進階的。

建立一些簡單的 Utility Types

今天這裡所提到的一些 Utility Types 讀者們可能會覺得有點雞肋,似乎不需要建立 Utility Types 就可以達到一樣的功能,不過相信我,等到後面掌握更多 TypeScript 的知識後,讀者將會有一種看到不同世界的感覺。

OrNull

先來看一下官方提到的 OrNull 這個 Utility Types,它的寫法是這樣:

// Utility Type
type OrNull<Type> = Type | null;

看起來非常單純,使用上可以像這樣:

type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';

// 使用 OrNull 這個 Utility Type 產生新的型別 ManufactureOrNull
type ManufactureOrNull = OrNull<Manufacture>;

const manufacture: ManufactureOrNull = 'Apple';

你會看到在使用 OrNull 這個 Utility Types 是,就像呼叫一個 function 一樣,我們把 Manufacture 當成參數透過 <> 傳入 OrNull 中,而它會回傳一個新的型別給我們。

ManufactureOrNull 實際上的值其實也就是 'Apple' | 'Google' | 'Samsung' | 'Sony' | null,雖然 OrNull 這個 Utility Types 看起來很雞肋,但有時其實還蠻好用的,因為它會讓我們的程式看起來比較乾淨一些:

function getManufacture(manufacture: Manufacture | null) {
/*...*/
}
function getManufacture(manufacture: OrNull<Manufacture>) {
/*...*/
}

舉例來說,上面這兩個 function 對於參數 manufacture 的型別定義雖然一樣,但後者個人看起來就更工整了一些,而不會有一種 null 是多加出來的感覺。

OneOrMany

再來一樣是官方有說明到的 OneOrMany,寫法是這樣:

// Utility Type
type OneOrMany<Type> = Type | Type[];

使用上像這樣:

type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';

// 使用 OneOrMany 這個 Utility Type 產生新的型別 ManufactureOrManufactures
type ManufactureOrManufactures = OneOrMany<Manufacture>;

const manufactures: ManufactureOrManufactures = ['Apple', 'Google'];

這裡一樣可以看到,我們把 Manufacture 當成參數一樣從 <> 傳進 OneOrMany 這個 Utility Type 中,並得到新的 ManufactureOrManufactures 型別,它的值只要滿足 Manufacture 或者是 Manufacture[] 都可以。

OneOrManyOrNull

Utility Types 就像函式一樣,所以也可以一個 Utility Type 包著另一個 Utility Type,例如:

// Utility Type
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

這裡 OneOrManyOrNull 是一個 Utility Type,而它是透過 OrNullOneOrMany 同時組合出來的,使用的方式一樣:

type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';

// 使用 OneOrManyOrNull 這個 Utility Type 產生新的型別 OneOrManyOrNullOfManufacture
type OneOrManyOrNullOfManufacture = OneOrManyOrNull<Manufacture>;

const manufacturesData: OneOrManyOrNullOfManufacture = null;

這裡一樣可以把 Manufacture 當成參數一樣傳入 OneOrManyOrNull 這個 Utility Type 中,現在 OneOrManyOrNullOfManufacture 指的就會是 Manufacture | Manufacture[] | null

物件型別的 Utility Types

從上面的幾個例子中,相信讀者應該可以感受到 Utility Types 能夠像 function 一樣,輸入 input 並取得 output 的感覺,只是 input 和 output 都需要是 TypeScript 的型別。

讓我們回顧一下昨天的 getObjValue 這個函式,實際上如果不管函式本身,針對物件型別也是可以建立出幾個不同的 Utility Types。

這裡我們先建立一個物件型別作為後面的範例:

type Manufacture = 'Apple' | 'Google' | 'Samsung' | 'Sony';
type Product = {
name: string;
price: number;
manufacturer: Manufacture;
isLaunched: boolean;
};

Keys

建立一個 Utility Type 來取出所有物件型別的 keys 且只要 string

// Utility Type
type Keys<T> = keyof T & string;

使用的方式:

type KeysOfProduct = Keys<Product>; // "name" | "price" | "manufacturer" | "isLaunched"

這時候 KeysOfProduct 的結果會是 "name" | "price" | "manufacturer" | "isLaunched"

讀者可以注意到,Product 這個物件型別一樣可以被當成參數一樣傳入 Keys 這個 Utility Type 的 <> 內。

Values

Values 這個 Utility Type 則是可以取出物件型別的所有屬性值(型別),讀者們可以根據昨天的練習試著自己寫寫看:

// Utility Type
type Values<T> = T[keyof T];

使用的方式一樣可以把物件型別 Product 當成參數帶入:

type ValuesOfProduct = Values<Product>; //  string | number | boolean

最後會得到新的型別 ValuesOfProduct

PickObj

最後來寫一個 Utility Type,它的作用是取出物件型別中的某個 key 的屬性值(型別),讀者們一樣可以根據昨天的內容試著練習看看:

// Utility Type
type PickObj<T, U extends keyof T> = T[U];

這裡一樣要記得 U extends keyof T 的使用,如果沒有限制泛型 U 一定要是物件型別 T 的 key,TypeScript 因為沒辦法確保能在 T 中找到 U 這個 key,將會報錯:

Type Utilities

使用上則是可以同時帶入兩個型別參數:

type Price = PickObj<Product, 'price'>; // number

如此就可以取出物件型別中 key 為 price 的屬性值的型別。

Utility Types 學習小技巧

今天提到的一些 Utility Types 多半還蠻直觀的,甚至可能會覺得可有可無,重點是讓讀者先對於可以把 Type 當成 function 一樣使用有些感覺,後面當我們學到 Conditional Types、Mapped Types 等等更進階的用法後,你將會發現「Wow!原來型別還可以這樣『玩』!」。

這裡提供一些未來在學習 Utility Types 是,方便好用的小技巧:

  • 在學習 Utility Types 是,把抽象的東西用一些具體的值帶入,可以幫助我們更好了解
  • 擅用 Type Alias,把 Utility Type 回傳的結果保存成一個 Type,可以更清楚看到該 Type Utility 的作用。

範例程式碼

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

參考資料