本文 AI 產出,尚未審核

TypeScript 工具型別(Utility Types)

主題:Partial<T>


簡介

在大型的 TypeScript 專案中,我們常會面臨「物件只需要部份屬性」的情境,例如更新表單、API 的 PATCH 請求,或是將資料切割成多個介面重組。若每次都手動把所有屬性改成可選 (?) ,不僅繁瑣,還容易遺漏或造成型別不一致。

Partial<T> 正是為了解決這類需求而設計的 工具型別。它能在編譯期自動把給定型別 T 的所有屬性變為可選,讓開發者只需要關注「哪些屬性會被提供」而不必重複撰寫冗長的型別宣告。

本篇文章將從概念、實作細節、常見陷阱到實務應用,完整說明 Partial<T> 的使用方式,適合 從初學者到中階開發者 的所有人閱讀與參考。


核心概念

什麼是 Partial<T>

Partial<T> 是 TypeScript 內建的 泛型工具型別,其宣告大致如下(實際實作在 lib.es5.d.ts 中):

type Partial<T> = {
  [P in keyof T]?: T[P];
};

簡單來說,它會遍歷 T 的所有屬性鍵 (keyof T),然後把每個鍵對應的屬性型別 T[P] 包裝成 可選 (?)。最終得到的型別是一個「所有屬性皆為可選」的物件型別。

重點Partial<T> 不會改變原本的型別,它只會回傳一個全新型別。原始型別仍然保持不變,這對於型別安全非常重要。

為什麼要使用 Partial<T>

場景 若不使用 Partial<T> 使用 Partial<T> 的好處
表單編輯 每次都手寫 interface EditUser { name?: string; age?: number; ... } 只要 Partial<User>,自動同步屬性變更
PATCH API 必須自行建立 type PatchUser = Partial<User> 或手動列出 直接傳入 Partial<User>,減少維護成本
函式參數 function update(opts: { name?: string; age?: number; ... }) function update(opts: Partial<User>),更具可讀性
測試資料 測試時常只需要部份欄位 Partial<User> 讓測試資料更彈性

Partial<T> 的實作原理

  1. 映射型別 ([P in keyof T]):遍歷 T 的所有屬性鍵。
  2. 可選屬性 (?:):在每個屬性後加上 ?,使其變為可選。
  3. 屬性型別保留 (T[P]):屬性的實際型別不變,只是變成可選。

如果 T 本身已經有可選屬性,Partial<T> 仍會保留可選狀態,且不會把必填屬性變成必填——全部都會是 可選


程式碼範例

以下示範 5 個常見且實用的 Partial<T> 用法,並配合註解說明每一步的意圖。

1️⃣ 基本範例:把完整介面變成可選

interface User {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
}

// 直接使用 Partial
type UpdateUserDTO = Partial<User>;

const update1: UpdateUserDTO = {
  // 只需要提供想要更新的欄位
  name: "Alice",
};

說明UpdateUserDTO 會變成 { id?: number; name?: string; email?: string; isAdmin?: boolean },因此在更新時只需要提供被修改的欄位即可。


2️⃣ 結合函式參數:實作通用的 update 函式

function updateUser(id: number, changes: Partial<User>) {
  // 假設有一個全域的 userMap
  const user = userMap.get(id);
  if (!user) throw new Error("User not found");

  // 使用 Object.assign 合併變更
  Object.assign(user, changes);
}

// 呼叫範例
updateUser(1, { email: "new@example.com", isAdmin: true });

技巧:因為 changesPartial<User>,IDE 會自動提供屬性補全,且不會允許不存在的屬性。


3️⃣ 與 Pick<T, K> 結合:只允許部份屬性可選

有時候我們只想讓 特定 屬性可選,而其他屬性仍保持必填。可以先 Pick 需要的屬性,再套用 Partial

// 只讓 name 與 email 可選,其他保持必填
type UserUpdate = Partial<Pick<User, "name" | "email">> & Omit<User, "name" | "email">;

const update2: UserUpdate = {
  id: 2,          // 必填
  isAdmin: false // 必填
  // name、email 可以省略或提供
};

重點Pick + Partial + Omit 的組合讓我們可以細緻控制哪些欄位是必填、哪些是可選。


4️⃣ 深層 Partial(遞迴版)

原生的 Partial<T> 只會把第一層屬性設為可選,若屬性本身是物件,內部屬性仍保持原本的必填狀態。以下示範自訂遞迴 DeepPartial<T>

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Profile {
  user: User;
  settings: {
    theme: string;
    notifications: boolean;
  };
}

// DeepPartial 讓所有子層都變可選
type PartialProfile = DeepPartial<Profile>;

const profilePatch: PartialProfile = {
  user: { email: "patched@example.com" }, // 只需要提供想改的欄位
  settings: { notifications: false }      // 同上
};

實務提醒:遞迴型別會增加編譯時間,僅在需要深層更新時使用。


5️⃣ 與類別結合:使用 Partial<T> 建構物件

class Product {
  constructor(
    public id: number,
    public name: string,
    public price: number,
    public tags: string[]
  ) {}
}

// 讓建構子接受 Partial<Product>
function createProduct(data: Partial<Product>): Product {
  // 預設值可以寫在這裡
  const defaults: Product = new Product(0, "Unnamed", 0, []);
  return Object.assign(defaults, data);
}

// 呼叫
const p = createProduct({ name: "Laptop", price: 1200 });

說明createProduct 允許呼叫者只提供需要的欄位,而剩餘欄位會使用預設值。這在「工廠函式」或「測試資料生成」時非常實用。


常見陷阱與最佳實踐

1️⃣ 過度使用 Partial<T> 造成型別過寬

如果把整個大型介面直接變成 Partial,可能會把不該被修改的欄位也變成可選,導致 API 接收不完整資料。建議:只在真正需要的情境(如 PATCH)使用,或搭配 PickOmit 限制範圍。

2️⃣ Partial 只作用於第一層

如前所述,Partial<User> 不會遞迴處理巢狀物件。若有深層結構且需要部份更新,請自行實作 DeepPartial 或使用第三方套件(如 type-festPartialDeep)。

3️⃣ readonly 結合的衝突

Partial<T> 只會把屬性變為可選,不會改變 readonly 修飾。如果你想同時把屬性設為可寫,需再結合 -readonly

type MutablePartial<T> = {
  -readonly [P in keyof T]?: T[P];
};

4️⃣ 在函式重載時的型別推斷

當同一函式接受完整型別或 Partial 時,TypeScript 可能無法正確推斷返回值型別。此時可以使用 條件型別函式重載 明確區分:

function saveUser(user: User): User;
function saveUser(user: Partial<User>): Partial<User>;
function saveUser(user: any): any {
  // implementation
}

5️⃣ 避免在公共 API 中直接暴露 Partial<T>

對外的 API(例如 npm 套件)若直接回傳 Partial<T>,使用者可能會依賴「可選」的屬性,未來若你改變了原始介面,會造成相容性問題。最佳實踐:在公開層面定義專屬的 DTO(Data Transfer Object),內部使用 Partial<T> 進行實作。


實際應用場景

場景 為什麼適合使用 Partial<T>
表單編輯 UI 使用 Partial<FormValues> 讓表單狀態只儲存變更過的欄位,減少不必要的重繪。
RESTful PATCH 後端只接受部份欄位更新,前端可直接傳 Partial<Resource>,型別安全且省去手寫介面。
Redux / Zustand 狀態合併 setState(prev => ({ ...prev, ...partial }))partial 可用 Partial<State> 表示。
測試資料生成 factory<User>(partial => ({ id: 1, name: "Bob", ...partial })),讓測試只關心需要的欄位。
微服務間資料同步 當服務 A 只需要更新服務 B 中的部分欄位時,使用 Partial<SharedModel> 來保證欄位名稱一致且不會遺漏。

總結

Partial<T> 是 TypeScript 提供的 高效、易用 的工具型別,讓開發者可以在不改動原始介面的前提下,快速產生「所有屬性皆為可選」的型別。透過本文的概念說明、實作範例與最佳實踐,你應該已經能夠:

  1. 正確使用 Partial<T> 於函式參數、API 請求、狀態管理等情境。
  2. 辨識 何時應該搭配 PickOmit 或自訂遞迴型別,以避免過度寬鬆的型別。
  3. 避免 常見的陷阱,如第一層限制、readonly 衝突與公共 API 的相容性問題。

在日常開發中,善用 Partial<T> 能大幅減少重複的型別宣告,提高程式碼可讀性與維護性。未來若遇到更複雜的「部份更新」需求,記得考慮 DeepPartial 或其他進階工具型別,讓型別安全與彈性兼得。祝你在 TypeScript 的世界裡寫出更乾淨、更可靠的程式碼!