本文 AI 產出,尚未審核

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,能避免重複寫 PickOmit


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> 只保留同時屬於 TU 的成員 `Extract<"a"
NonNullable<T> 移除 nullundefined `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」時,ReturnTypeParameters 能確保新函式的簽名與原函式保持一致,減少手動同步的錯誤。


常見陷阱與最佳實踐

陷阱 說明 解決方式
過度使用 any 逃避型別檢查 在複雜的泛型運算時,一時想快就把 any 丟進去,會失去 Utility Types 的好處。 儘量使用 unknown 搭配型別保護 (type guard) 來保留型別資訊。
Partial 與必填欄位混用 Partial<T> 用在本應完整的資料上,導致跑時缺少必要屬性。 在 API 輸入與內部模型之間明確分離型別,例如 type CreateUserDTO = Partial<User>,但在服務層仍使用 User
Pick / Omit 的鍵名錯誤 鍵名打錯或忘記更新時,編譯器不一定能直接提示。 使用 as constkeyof 產生鍵名集合,讓編譯器檢查鍵名正確性。
Record 的鍵值不一致 Record<string, number> 允許任意字串鍵,若實際只接受特定字串會失去限制。 儘量使用具體的聯合鍵 (`Record<"a"
ReturnType / Parameters 在 overloaded 函式上 只會取得最後一個 overload 的型別,可能不是你想要的。 為每個 overload 分別定義型別或使用條件型別自行抽取。

最佳實踐

  1. 盡量在型別層面解決問題:使用 PartialPick 等工具型別,避免在程式碼中寫大量 if (obj.prop !== undefined) 的檢查。
  2. 保持型別與資料來源一致:API 回傳的型別若為可選,使用 Partial<T>;若在內部必須完整,透過 Required<T> 轉換。
  3. 善用 as const:讓字面量自動推斷為字串聯合型別,配合 PickRecord 使用,提升型別安全。
  4. 寫型別保護函式:在接受 unknownany 的入口函式中,使用 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 方法,我們想寫一個「統一的呼叫」函式,使用 ReturnTypeParameters 讓 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

透過 PickOmit,快速產生只需要 idDelete 參數、或是需要全部欄位的 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 的型別世界裡玩得開心、寫得更佳!