TypeScript 進階型別操作 – Utility Types(工具型別)
簡介
在日常的前端開發中,我們常常需要對既有的型別做「切割」或「組合」的操作。手動寫出繁雜的型別定義不僅會讓程式碼變得冗長,還容易因為忘記更新而產生型別不一致的 bug。TypeScript 提供的 Utility Types(工具型別)正是為了解決這類問題而設計的,它們是內建在 typescript 套件裡的型別工具,能在編譯階段自動產生新型別,讓開發者以更簡潔、可讀的方式描述資料結構。
掌握這些工具型別不只是提升開發效率,更能在大型專案中維持型別的正確性與可維護性。本文將從核心概念出發,透過實用範例說明常見的 Utility Types,並討論使用時的陷阱與最佳實踐,最後帶出在真實專案中的應用情境。
核心概念
1. Partial<T>
Partial<T> 會將型別 T 的所有屬性變成可選 (?)。常用於 更新資料 或 表單填寫 的情境。
interface User {
id: number;
name: string;
email: string;
}
/* 只需要提供想要更新的欄位 */
type UpdateUser = Partial<User>;
const update: UpdateUser = {
name: "Alice", // 只提供 name 即可
// id、email 都是可選的
};
小技巧:在 Redux reducer 中,使用
Partial<State>來描述部分更新的 payload,能避免重複寫Pick或Omit。
2. Required<T>
相反地,Required<T> 會把所有屬性變成必填。常見於 將外部資料 (例如 API 回傳的 Partial) 轉換成內部必須完整的型別。
type OptionalUser = {
id?: number;
name?: string;
};
type CompleteUser = Required<OptionalUser>;
const user: CompleteUser = {
id: 1,
name: "Bob", // 必須同時提供所有欄位
};
3. Pick<T, K> 與 Omit<T, K>
Pick<T, K>:從型別T中挑選出屬性K(可為字串聯集)形成新型別。Omit<T, K>:從型別T中排除屬性K,保留剩餘欄位。
interface Product {
id: number;
name: string;
price: number;
description: string;
}
/* 只需要 id 與 name 的簡易版 */
type ProductSummary = Pick<Product, "id" | "name">;
/* 從 Product 中移除 description,得到可編輯的型別 */
type EditableProduct = Omit<Product, "description">;
實務上:當你在建立 API 回傳的 DTO(Data Transfer Object)時,
Pick可以快速產生「只需要的欄位」型別,避免手動維護多個相似介面。
4. Record<K, T>
Record 用來建立一個 鍵值對 的型別,其中鍵 (K) 必須是字串、數字或 symbol 的聯集,值的型別則為 T。這在 設定字典、映射表 時非常方便。
type Role = "admin" | "editor" | "viewer";
/* 每個角色對應的權限清單 */
const rolePermissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
5. Exclude<T, U>、Extract<T, U>、NonNullable<T>
| Utility | 功能 | 範例 |
|---|---|---|
Exclude<T, U> |
從聯合型別 T 中排除屬於 U 的成員 |
`Exclude<"a" |
Extract<T, U> |
只保留同時屬於 T 與 U 的成員 |
`Extract<"a" |
NonNullable<T> |
移除 null 與 undefined |
`NonNullable<string |
type APIResponse = "success" | "error" | null | undefined;
/* 只保留有效的狀態字串 */
type ValidStatus = Exclude<APIResponse, null | undefined>;
const status: ValidStatus = "success"; // 合法
6. ReturnType<T> 與 Parameters<T>
ReturnType<T>:取得函式型別T的回傳型別。Parameters<T>:取得函式型別T的參數型別(以 tuple 形式呈現)。
function fetchUser(id: number) {
return Promise.resolve({ id, name: "Alice" });
}
/* 取得 fetchUser 的回傳型別 */
type FetchUserReturn = ReturnType<typeof fetchUser>; // Promise<{ id: number; name: string; }>
/* 取得 fetchUser 的參數型別 */
type FetchUserParams = Parameters<typeof fetchUser>; // [number]
應用:當你要為一個已存在的函式建立「wrapper」或「proxy」時,
ReturnType與Parameters能確保新函式的簽名與原函式保持一致,減少手動同步的錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
過度使用 any 逃避型別檢查 |
在複雜的泛型運算時,一時想快就把 any 丟進去,會失去 Utility Types 的好處。 |
儘量使用 unknown 搭配型別保護 (type guard) 來保留型別資訊。 |
Partial 與必填欄位混用 |
把 Partial<T> 用在本應完整的資料上,導致跑時缺少必要屬性。 |
在 API 輸入與內部模型之間明確分離型別,例如 type CreateUserDTO = Partial<User>,但在服務層仍使用 User。 |
Pick / Omit 的鍵名錯誤 |
鍵名打錯或忘記更新時,編譯器不一定能直接提示。 | 使用 as const 或 keyof 產生鍵名集合,讓編譯器檢查鍵名正確性。 |
Record 的鍵值不一致 |
Record<string, number> 允許任意字串鍵,若實際只接受特定字串會失去限制。 |
儘量使用具體的聯合鍵 (`Record<"a" |
ReturnType / Parameters 在 overloaded 函式上 |
只會取得最後一個 overload 的型別,可能不是你想要的。 | 為每個 overload 分別定義型別或使用條件型別自行抽取。 |
最佳實踐:
- 盡量在型別層面解決問題:使用
Partial、Pick等工具型別,避免在程式碼中寫大量if (obj.prop !== undefined)的檢查。 - 保持型別與資料來源一致:API 回傳的型別若為可選,使用
Partial<T>;若在內部必須完整,透過Required<T>轉換。 - 善用
as const:讓字面量自動推斷為字串聯合型別,配合Pick、Record使用,提升型別安全。 - 寫型別保護函式:在接受
unknown或any的入口函式中,使用is關鍵字寫自訂型別守衛,配合 Utility Types 確保後續程式碼的型別正確。
實際應用場景
1. 表單資料的增量更新
在 React 中,表單狀態通常是 部分 更新的。使用 Partial<T> 可以讓 setState 的型別自動限制只能填寫表單欄位。
interface ProfileForm {
username: string;
email: string;
bio?: string;
}
type ProfileFormPatch = Partial<ProfileForm>;
function useProfileForm(initial: ProfileForm) {
const [state, setState] = useState<ProfileForm>(initial);
const update = (patch: ProfileFormPatch) => {
setState(prev => ({ ...prev, ...patch }));
};
return { state, update };
}
2. 多語系字典(i18n)
利用 Record<Locale, string> 建立語系對照表,結合 keyof 產生安全的取值函式。
type Locale = "en" | "zh-TW" | "ja";
const i18n: Record<Locale, Record<string, string>> = {
en: { greeting: "Hello" },
"zh-TW": { greeting: "哈囉" },
ja: { greeting: "こんにちは" },
};
function t(locale: Locale, key: keyof typeof i18n["en"]) {
return i18n[locale][key];
}
const msg = t("zh-TW", "greeting"); // "哈囉"
3. API Wrapper 的型別安全
假設有多個 API 方法,我們想寫一個「統一的呼叫」函式,使用 ReturnType 與 Parameters 讓 wrapper 完全繼承原始函式的型別。
async function getUser(id: number) {
// ...
return { id, name: "Alice" };
}
async function getPosts(userId: number) {
// ...
return [{ id: 1, title: "Post" }];
}
/* 統一的 API 呼叫 */
async function apiCall<T extends (...args: any) => Promise<any>>(
fn: T,
...args: Parameters<T>
): Promise<ReturnType<T>> {
return fn(...args);
}
/* 使用 */
const user = await apiCall(getUser, 10); // 型別推斷為 Promise<{ id: number; name: string; }>
const posts = await apiCall(getPosts, 10); // 型別推斷為 Promise<{ id: number; title: string; }[]>
4. 動態生成 CRUD Service
透過 Pick 與 Omit,快速產生只需要 id 的 Delete 參數、或是需要全部欄位的 Create 參數。
interface Article {
id: number;
title: string;
content: string;
authorId: number;
}
/* 建立新文章只需要除了 id 之外的欄位 */
type CreateArticleDTO = Omit<Article, "id">;
/* 刪除文章只需要 id */
type DeleteArticleDTO = Pick<Article, "id">;
總結
Utility Types 是 TypeScript 內建的 型別魔法師,它們讓開發者能在編譯階段即完成「屬性切割、合併、轉換」等常見需求,減少手寫冗長的型別宣告,也能提升程式碼的可讀性與可維護性。本文重點回顧如下:
| Utility | 主要用途 |
|---|---|
Partial<T> |
把所有屬性變成可選,適合增量更新或表單填寫 |
Required<T> |
把所有屬性變成必填,確保資料完整性 |
Pick<T, K> / Omit<T, K> |
挑選或排除特定屬性,快速產生子型別 |
Record<K, T> |
建立鍵值對字典,適合映射表或 i18n |
Exclude<T, U> / Extract<T, U> / NonNullable<T> |
操作聯合型別,過濾不需要的成員 |
ReturnType<T> / Parameters<T> |
從函式型別抽取回傳值與參數,寫 wrapper 時必備 |
在實務開發中,善用這些工具型別 能讓你的 TypeScript 程式碼更簡潔、更安全。記得在使用時留意型別的正確範圍、避免過度使用 any,並搭配型別保護與 as const,即可在大型專案中保持型別的一致性與可維護性。祝你在 TypeScript 的型別世界裡玩得開心、寫得更佳!