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 而不是自行定義新介面?
- 避免重複程式碼:當原始介面變更時,所有使用
Omit的派生型別會自動同步更新。 - 提升可讀性:
Omit<User, 'password'>一眼就能看出意圖,比起手寫type UserWithoutPwd = { id: number; name: string; email: string }更直觀。 - 減少維護成本:若新增欄位,只要在原始介面加上,
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同時利用了Omit與Partial,展現了工具型別的組合威力。
範例 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>。 |
最佳實踐
- 盡量使用字面量型別(例如
'password')而非字串變數,讓編譯器能在編譯期即檢查正確性。 - 搭配其他工具型別(
Partial,Pick,Required)形成「組合型別」,提升彈性。 - 在 API DTO(Data Transfer Object)層使用
Omit,確保外部只能看到允許的欄位。 - 保持原始介面的單一職責:若原始型別過於龐大,考慮拆成多個小介面,再用
Omit/Pick組合。 - 使用
as const讓字面量鍵保持字面型別,避免因推斷為string而失去鍵的約束。
實際應用場景
- 前端 UI 表單
FormValues = Omit<User, 'id' | 'createdAt'>→ 只保留使用者可編輯的欄位。
- 後端資料庫模型 vs. 回傳 DTO
UserEntity包含敏感欄位(passwordHash,salt),而回傳給前端的UserDTO = Omit<UserEntity, 'passwordHash' | 'salt'>。
- 多語系內容管理
LocalizedString = Omit<BaseString, 'id' | 'key'>→ 只保留語系文字本身。
- React/Redux 狀態管理
- 在
reducer中,更新動作的 payload 常用Partial<Omit<State, 'id'>>,避免意外改變不可變的id。
- 在
- 自動產生測試資料
MockUser = Omit<User, 'id'> & { id: string }→ 先排除自動產生的id,再自行指定測試用的id。
總結
Omit<T, K> 是 TypeScript 工具型別中最常被使用且最具威力的之一。它讓開發者可以:
- 快速從既有型別排除不需要的屬性,保持型別定義的單一真相(Single Source of Truth)。
- 與其他工具型別(
Partial、Pick、Required)自由組合,產生高度彈性的派生型別。 - 在 API、表單、資料庫模型等多種情境下,確保資料的正確性與安全性。
只要掌握 Omit 的基本語法、注意常見陷阱,並配合最佳實踐,就能在日常開發中寫出更乾淨、可維護且安全的 TypeScript 程式碼。未來面對更複雜的型別需求時,別忘了先思考是否可以透過 Omit 來簡化模型,讓型別系統為你把關,而不是成為負擔。祝開發順利!