[TypeScript] 用生活例子圖解 Utility Types

Hannah Lin
Hannah Lin
Published in
20 min readJan 31, 2024

TypeScript 系列文

1. The Very Basics for TS
2.
Generics 的使用情境
3. The `extends` keyword
4. Generics — Mapped Types
5. 用生活例子圖解 Utility Types
6. 優雅的在 React 中使用 TS
7.
用 ts-migrate 仙女棒讓 JS 專案瞬間 migrate 成 TS

TS 本身有許多內建的 Utility Types 方便常見的型別轉換,幫助我們在建立或定義客製 type 時更快速,先放一個我覺得很不錯的三分鐘入門影片讓你快速進入 Utility Type 的世界

接下來文章是我消化過後的圖解!開始之前必須要知道不同 type 會有不同對應的 Utility Types,所以我大致分成五大類 type,每一類都有屬於他們自己的 Utility Types,五大類分別為 Object Type 、Union Type、 Function Type、Promise 、 String Manipulation Types

🔖 文章索引

1. Object Type
2. Union Type
3. Function Type
4. Promise
5. String Manipulation Types

Note. 為了好理解我會把這些 Utility types 原始定義方法給列出來,實際上使用是不需要寫的

Object type

把屬性設為 optional的 Partial<T>、 屬性全部為 required 的 Required<T>、把特定屬性踢掉的 Omit<T, K> 、只想要特定屬性的 Pick<T, K>,以及Readonly<T>Record<K,T> ,原理其實都是為上一篇 mapped type 的應用

// 我就是一個 Object type
type User = {
id: string;
name: string;
age: number;
}

以生活中例子來說 Object Type 就是一個抽抽樂中的桶子,裡面有很多等著被抽中的紙卡 ( properties ,或稱作 Keys),Utility types 則提供不同的抽獎方式

這圖做了兩小時,已是我絞盡腦汁能想到的易懂圖了

Partial<T>

Creates a type that makes all properties in T optional
我可以選擇要不要抽獎 : Object type 裡面的紙卡 (property) 是可有可無的

/*
Partial<T>: 把 T 裡的所有屬性都設為 optional
* T 就是傳進來的 Object Type
* P: 就是 property 屬性
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

Partial<T> 回傳一個新的物件型別,會將傳進來的<T>這個物件型別中所有屬性都設定為 optional 。

type User = {
id: string;
name: string;
age: number;
}
type PartialUser = Partial<User>

/* Inferred Type
{
id?: string|undefined;
name?: string|undefined;
age?: number|undefined;
};
*/

// 以下三種都給過,屬性不必全部都寫
const A: PartialUser = {
id: 'id',
name: 'Hannah',
age: 18
}
const b: PartialUser = {
id: 'id',
}
const c: PartialUser = {}
因為是 Partial,所以以上三種狀況都是可以的

Required<T>

Creates a type that all of the properties are required
我要把所有紙卡都抽出來!

/*
Required<T>: 把 T 裡的所有屬性都設為必填
* T 就是傳進來的 Object Type
* P: 就是 property 屬性
* - 這個符號就是把所有擁有 ? 的屬性移除
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};

Required<T> 回傳一個新的物件型別,會將傳進來的<T>這個物件型別中所有屬性都設定為必填,和 Partial<Type> 正好相反。

type User = {
id: string|undefined;
name: string|undefined;
age: number;
}
type RequiredUser = Required<User>

/* Inferred Type
{
id: string;
name: string;
age: number;
};
*/

const A: RequiredUser = {
id: 'id',
name: 'Hannah',
age: 18
}
// error: 少了 name 跟 age 屬性
const b: RequiredUser = {
id: 'id',
}

Pick<T, K>

Creates a type that only get the properties that we pass to pick
Object type 這個抽抽樂盒子為透明,我要抽出特定的紙卡 (keys)

/*
Pick<T,K>: 只取得在 T 裡面的 K
* T 是傳進來的 Object Type
* K(為 string or Union type) 是想取得的 keys
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Pick<T,K> 用途在把傳進來的<T>這個物件型別中選取一組屬性 K(string or union)來建立一個新型別。

type PickUser = Pick<User, "id" | "name">

/* Inferred Type
{
id: string;
name: string;
};
*/
const A: PickUser = {
id: 'id',
name: 'Hannah',
}
// error: 'age' does not exist in type 'PickUser'
const b: PickUser = {
id: 'id',
age: 18
}

Omit<T, K>

Creates a type that remove properties we pass to omit
在抽獎前把不需要的紙卡 (keys) 丟掉,然後剩下的我整盒都要

/*
Omit<T, K>: 把 T 這個 object type 裡的 K 都踢掉
* T 是傳進來的 Object Type
* K 就是 keys (為 Union type),可以是任何屬性名稱
* Exclude<keyof T, K> : 把 K 這個屬性從 T 的所有屬性中除掉
* Pick<T, keys>: 回傳 keys 這個 Object Type
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Omit<T,K> 用途在把傳進來的<T>這個物件型別中排除一組屬性 K(string or union)來建立一個新型別,和 Pickt<T,K> 正好相反。以 User 這個 Object Type 來說,Omit<User, “id” | “name”>等於Pick<User, “age”>

type User = {
id: string;
name: string;
age: number;
}
type OmitUser = Omit<User, "id" | "name">
/* Inferred Type
{ age: number };
*/

const A: OmitUser = {
age: 18,
}
// error: 'id' does not exist in type 'OmitUser'.
const b: OmitUser = {
id: 'id',
age: 18
}

Record<K, T>

Construct a type with a set of properties K of type T
這個跟抽抽樂無關,是先寫你要的紙卡們 (keys),然後把它們通通指定為特定型別 (可以是 object type 或 boolean, number 等任何型別)

/*
Record<K, T>: 使 K 的型別為 T
* T 是想要的型別,任何型別都可
* K 就是 keys (為 Union type),可以是任何屬性名稱
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

Record<Keys, Type> 用來將一種型別的屬性 mapped 到另一種型別。很適合拿來做各種排列組合使用,例如抓已有的 type key 然後指派成其他型別

// Inferred Type: {darkMode: boolean, mobile: boolean}
Record<"darkMode" | "mobile", boolean>

// Inferred Type: {[x: string]: boolean}
Record<any, boolean>

/**
Inferred Type:
{ hi : {
name: string;
color: string;
count: number;
}}
*/
Record<'hi', Fruit>

Readonly<T>

Creates a type with all properties of another type set to readonly.
所有屬性只能讀取不能寫入

/*
Readonly<T>: 把 T 裡的所有屬性都設為只能讀取不能寫入
* T 是傳進來的 Object Type
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Readonly<Type> 這個工具型別也會回傳一個新的型別,主要是將Type 的所有屬性都變成唯讀。

type ReadonlyUser = Readonly<User>

/* Inferred Type
{
readonly id: string;
readonly name: string;
readonly age: number;
};
*/

Multiple utility types

實務上也可以一次可以用好幾個 utility types 來達到你想要的

type User = {
id: string;
name: string;
age: number;
}

/* Inferred Type
{
id?: any;
name?: any;
};
*/
type TUtils = Partial<Pick<Record<string, any>, 'id' | 'name'>>

Union Types

移除掉特定值的Exclude<T,U> 、只抓取特定值的 Extract<T, U>、把所有 null 跟 undefined 移除的 NonNullable<T>

// 我是一個 Union type
type Role = "admin" | "user" | "anonymous"

以生活中例子來說 Union type 就是你手中的牌,Utility types 則是用不同方式讓你決定留在你手中的牌該有哪幾張

Exclude<T, U>

Creates a type help to remove members of a union
把手中不想要的牌丟掉

/*
Exclude<T, U>: 把 T 裡的 U 移除
* T 是一個 union type
* U 就要移除的 Union,也就是 Exclude members,可以是 primitive 值或 Object
* `never` 代表 the type of values that never occur,也就是不要取值
*/
type Exclude<T, U> = T extends U ? never : T;

Exclude<T, U>用途在把傳進來的<T>這個 Union Type 中排除一組屬性 U(string or union)來建立一個新新的 Union Type。

type Role = "admin" | "user" | "anonymous"
type ExcludeUser = Exclude<Role, "admin" | "user">
// Inferred Type: "anonymous"

const Result: ExcludeUser = "anonymous"

type A = Exclude<1 | 2 | 3, 2>;
// Inferred Type: 1 | 3

type B = Exclude<1 | "a" | 2 | "b", number>;
// Inferred Type: "a" | "b"

type C = Exclude<1 | "a" | 2 | "b", 1 | "b" | "c">;
// Inferred Type: "a" | 2

甚至還有一些比較進階用法可以移除含有特定結尾名稱的 type,例如 ${string}${“id” | “Id”}${string}

// 範例ㄧ
/* Remove strings with a certain prefix/suffix from a union */
type ObjectKey =
| "userId"
| "postId"
| "id"
| "userName"
| "postName";

type NonNameKey = Exclude<ObjectKey, `${string}Name`>;
// Inferred Type: "userId" | "postId" | "id"


// 範例二
/* Remove strings with one of several possible values from a union */
type ObjectKey =
| "userId"
| "postId"
| "id"
| "userName"
| "postName";

type NonIdKey = Exclude<
ObjectKey,
`${string}${"id" | "Id"}${string}`
>;
// Inferred Type: "userName" | "postName"

被丟棄的 type 不止原始值,甚至連 Object 也可以

// 範例三
/* Exclude members of a discriminated union by shape */
type Routes =
| {
route: "/user";
search: {
id: string;
};
}
| {
route: "/user/create";
}
| {
route: "/user/edit";
search: {
id: string;
};
};

type RoutesWithoutSearch = Exclude<
Routes,
{
search: any;
}
>;
// Inferred Type: { route: "/user/create" }

Extract<T, U>

Creates a type help to grab out a specific member of the union
把手中想要的牌留起來,其他丟掉

/*
Exclude<T, U>: 只抓取 T 裡的 U
* T 是一個 union type
* U 要抓取的 Union
*/
type Extract<T, U> = T extends U ? T : never;

Extract<T, U>用途在把傳進來的<T>這個 Union Type 中選取一組屬性 U(string or union)來建立一個新的 Union Type ,和 Exclude<T,U> 正好相反。

type Role = "admin" | "user" | "anonymous"
type ExtractUser = Extract<Role, "admin" | "user">

/* Inferred Type:
type ExtractUser = "admin" | "user"
*/

type Extract<A, B> = A extends B ? A : never;

// Inferred Type: 1 | 2
type Result1 = Extract<1 | "a" | 2 | "b", number>;

// Inferred Type: 1 | "b"
type Result2 = Extract<1 | "a" | 2 | "b", 1 | "b" | "c">;

NonNullable<T>

Creates a type that excluding null and undefined from T
type 不能為 nullundefined

/*
NonNullable<T>: 把 T 裏面的null 或 undefined 移除
* T 是一個 union type
*/
type NonNullable<T> = T & {};

NonNullable<T> 這個工具型別會把傳進來的 Union Type<T>中的 null and undefined都排除,然後回傳一個新的 Union Type。

type MaybeStrting = string | null | undefined
type DefinitelyString = NonNullable<MaybeStrting>

/* Inferred Type
type DefinitelyString = string
*/

Function Types

抓取回傳值型別的 ReturnType<T>、抓取參數型別的 Parameters<T>

// 我是一個 Function type
type Func = (a: number, b: number) => number

ReturnType<T>

If we want to access the type return from function
建立一個新的型別為回傳值的 type

/*
ReturnType<T>: 為回傳值的 type
* T 是一個 function type
* `infer R`表示待推斷的回傳值型別
*/
type ReturnType<T extends (...args: any) => any
> = T extends (...args: any) => infer R ? R : any;
type Func = (a: number, b: number) => number
type ReturnValue = ReturnType<Func>

/* Inferred Type
type ReturnValue = number
*/

type a = ReturnType<() => void> // void
type b = ReturnType<() => string | number> // string | number
type c = ReturnType<() => any> // any

Parameters<T>

Get tuple of the function parameters
建立一個新的型別為函示參數的 type

/*
Parameters<T>: 為函示參數的型別
* T 是一個 function type
* `infer P`表示待推斷的參數型別
*/
export type Parameters<T extends (...args: any) => any
> = T extends (...args: infer P) => any
? P
: never;
type Func = (a: number, b: number) => number
type Params = Parameters<Func>

/* Inferred Type
type Params = [a: number, b: number]
*/

Promise Type

抓取回傳值型別的 ReturnType<T>、抓取參數型別的 Parameters<T>

// 我是一個 Promise type
type PromiseStr = Promise<string>
type Result = Awaited<PromiseStr>

Awaited<Type>

This type is meant to model operations like await in async functions, or the .then() method on Promise - specifically, the way that they recursively unwrap Promises

const func = async () => ({
id: 123
})

// Inferred Type: Promise<{id: number}>
type Result1 = ReturnType<typeof func>
// Inferred Type: {id: number}
type Result2 = Awaited<ReturnType<typeof func>>

String Manipulation Types

簡立新的型別轉換你傳進來的字串為大寫 Uppercase<StringType>、小寫 Lowercase<StringType>、首字大寫 Capitalize<StringType>、取消首字大血 Uncapitalize<StringType>等等。

type UppercaseWes = Uppercase<"wes">;
// type UppercaseWes = "WES"

type LowercaseWes = Lowercase<"Wes">;
// type LowercaseWes = "wes"

type CapitalizeWes = Capitalize<"wes">;
// type CapitalizeWes = "Wes"

type UncapitalizeWes = Uncapitalize<"Wes">;
// type UncapitalizeWes = "wes"

以下例子就是當你想轉換 key 值為首字大寫時,用 String Manipulation Types 方法操作就非常容易。

export type CharacterType = {
name: string;
alignment: string;
intelligence: number;
strength: number;
speed: number;
durability: number;
power: number;
combat: number;
total: number;
};

type TableRowProps = {
heading: Capitalize<keyof CharacterType>;
value: CharacterType[keyof CharacterType];
};

/* Inferred Type
type TableRowProps = {
heading:
| "Name"
| "Alignment"
| "Intelligence"
| "Strength"
| "Speed"
| "Durability"
| "Power"
| "Combat"
| "Total";
value: string | number;
};
*/

整理圖表

把 utility type 整理成圖表之後查找比較容易

Mapped type

Union Type

--

--