本文 AI 產出,尚未審核

TypeScript 工具型別(Utility Types)

主題:Omit<T, K>


簡介

在大型 TypeScript 專案中,型別的重用與組合是提升開發效率、減少錯誤的關鍵。
Omit<T, K> 這個工具型別讓我們可以從既有介面或型別中排除特定屬性,產生新的型別,而不必手動重新撰寫或複製整個結構。

想像你在開發一個表單系統,User 介面包含許多欄位,但在某些 API 呼叫只需要「不含密碼」的資料。此時 Omit 可以瞬間幫你產出正確的型別,確保編譯期就捕捉到欄位遺漏或多餘的錯誤,大幅提升程式碼的安全性與可維護性。


核心概念

什麼是 Omit<T, K>

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
  • T:原始型別(通常是介面或型別別名)。
  • K:要被排除的屬性名稱集合,可以是單一屬性或多個屬性的聯集 ('a' | 'b')。
  • Omit 內部其實是先使用 Exclude<keyof T, K> 取得「剩下的鍵」集合,再用 Pick<T, …> 把這些鍵挑出來形成新型別。

重點Omit 不會改變原始型別,它回傳的是一個全新的型別,保持 immutable 的特性。

為什麼要用 Omit 而不是自行定義新介面?

  1. 避免重複程式碼:當原始介面變更時,所有使用 Omit 的派生型別會自動同步更新。
  2. 提升可讀性Omit<User, 'password'> 一眼就能看出意圖,比起手寫 type UserWithoutPwd = { id: number; name: string; email: string } 更直觀。
  3. 減少維護成本:若新增欄位,只要在原始介面加上,Omit 派生的型別自動包含新欄位(除非被排除)。

程式碼範例

以下示範 5 個實務中常見的 Omit 用法,每個範例都附上說明與註解。

範例 1:排除單一屬性(最基本的用法)

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 產生一個不含 password 的型別
type PublicUser = Omit<User, 'password'>;

const user: PublicUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  // password: 'secret', // ❌ TypeScript 會報錯:屬性不存在
};

說明PublicUser 只保留 User 中除 password 之外的屬性,適合回傳給前端或第三方 API。

範例 2:排除多個屬性

interface Product {
  id: number;
  name: string;
  price: number;
  cost: number;      // 只在內部使用的成本
  createdAt: Date;
}

// 只需要對外顯示的欄位
type PublicProduct = Omit<Product, 'cost' | 'createdAt'>;

const p: PublicProduct = {
  id: 101,
  name: 'Keyboard',
  price: 1999,
  // cost: 1200,          // ❌ 編譯錯誤
  // createdAt: new Date() // ❌ 編譯錯誤
};

說明:一次排除多個屬性,讓型別更貼近「公開」資料結構。

範例 3:結合 Partial 產生「可選」的更新型別

PATCH API 中,我們常只需要傳入要變更的欄位。可以先 Omit 掉不允許更新的欄位,再用 Partial 讓剩餘欄位變為可選:

interface Article {
  id: string;
  title: string;
  content: string;
  authorId: string;
  createdAt: Date;
}

// 只能更新 title、content,且兩者皆可選
type UpdateArticleDTO = Partial<Omit<Article, 'id' | 'authorId' | 'createdAt'>>;

const update: UpdateArticleDTO = {
  title: 'New Title' // content 可以不提供
};

說明UpdateArticleDTO 同時利用了 OmitPartial,展現了工具型別的組合威力。

範例 4:在泛型中使用 Omit(高度抽象)

// 通用的 CRUD Service
class BaseService<T> {
  // 只接受除 id 以外的欄位作為建立資料
  create(data: Omit<T, 'id'>): T {
    // 假設會自動產生 id
    const newItem = { ...data, id: generateId() } as T;
    // 儲存到資料庫...
    return newItem;
  }
}

// 具體的使用者 Service
interface User {
  id: string;
  name: string;
  email: string;
  password: string;
}

class UserService extends BaseService<User> {}

const us = new UserService();
const newUser = us.create({
  name: 'Bob',
  email: 'bob@example.com',
  password: '123456',
  // id: 'xxx' // ❌ 不能自行提供 id
});

說明:在泛型 BaseService<T> 中,用 Omit<T, 'id'> 把「自動產生」的欄位排除,讓子類別自動繼承正確的建立型別。

範例 5:使用 keyof 動態決定要排除的屬性

type SensitiveKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never
}[keyof T];

// 取得所有字串屬性(可能是敏感資訊)
type UserSensitive = SensitiveKeys<User>; // 'name' | 'email' | 'password'

// 用 Omit 動態排除
type SafeUser = Omit<User, UserSensitive>;

const safe: SafeUser = {
  id: 'u001',
  // name, email, password 都被排除
};

說明:透過條件型別與映射型別,我們可以動態算出要排除的鍵,再交給 Omit 完成型別的過濾,適合大型專案的安全檢查。


常見陷阱與最佳實踐

陷阱 說明 解決方式
K 不是 T 的鍵 K 包含不存在於 T 的屬性,TypeScript 會直接錯誤。 使用 K extends keyof T 限制,或先使用 Extract<K, keyof T>
排除後變成空型別 K 包含所有鍵時,Omit<T, K> 會產生 {},可能導致意外的寬鬆型別。 確認排除的鍵集合合理,或在需要時加上 never 交叉檢查。
與交叉類型 (&) 結合時的分布性 Omit<A & B, 'x'> 只會從 A & B 的聯合鍵中排除 'x',但如果 'x' 只在 A 中,B 仍保留。 若想要同時排除於多個型別,分別 Omit<A, 'x'> & Omit<B, 'x'>
深層結構無法直接 Omit Omit 只會作用於最外層屬性,無法直接排除巢狀物件的子屬性。 需要自行寫深層版的 DeepOmit(利用遞迴條件型別)或手動組合。
Pick 混用時的鍵重複 Pick<Omit<T, 'a'>, 'b'> 仍然合法,但如果 'b' 已被排除,會得到 never 確認 Pick 的鍵在 Omit 後仍然存在,或使用 Extract<keyof T, K>

最佳實踐

  1. 盡量使用字面量型別(例如 'password')而非字串變數,讓編譯器能在編譯期即檢查正確性。
  2. 搭配其他工具型別Partial, Pick, Required)形成「組合型別」,提升彈性。
  3. 在 API DTO(Data Transfer Object)層使用 Omit,確保外部只能看到允許的欄位。
  4. 保持原始介面的單一職責:若原始型別過於龐大,考慮拆成多個小介面,再用 Omit/Pick 組合。
  5. 使用 as const 讓字面量鍵保持字面型別,避免因推斷為 string 而失去鍵的約束。

實際應用場景

  1. 前端 UI 表單
    • FormValues = Omit<User, 'id' | 'createdAt'> → 只保留使用者可編輯的欄位。
  2. 後端資料庫模型 vs. 回傳 DTO
    • UserEntity 包含敏感欄位(passwordHash, salt),而回傳給前端的 UserDTO = Omit<UserEntity, 'passwordHash' | 'salt'>
  3. 多語系內容管理
    • LocalizedString = Omit<BaseString, 'id' | 'key'> → 只保留語系文字本身。
  4. React/Redux 狀態管理
    • reducer 中,更新動作的 payload 常用 Partial<Omit<State, 'id'>>,避免意外改變不可變的 id
  5. 自動產生測試資料
    • MockUser = Omit<User, 'id'> & { id: string } → 先排除自動產生的 id,再自行指定測試用的 id

總結

Omit<T, K> 是 TypeScript 工具型別中最常被使用且最具威力的之一。它讓開發者可以:

  • 快速從既有型別排除不需要的屬性,保持型別定義的單一真相(Single Source of Truth)。
  • 與其他工具型別(PartialPickRequired)自由組合,產生高度彈性的派生型別。
  • 在 API、表單、資料庫模型等多種情境下,確保資料的正確性與安全性。

只要掌握 Omit 的基本語法、注意常見陷阱,並配合最佳實踐,就能在日常開發中寫出更乾淨、可維護且安全的 TypeScript 程式碼。未來面對更複雜的型別需求時,別忘了先思考是否可以透過 Omit 來簡化模型,讓型別系統為你把關,而不是成為負擔。祝開發順利!