TypeScript 基本型別 – Intersection Type(交集型別)
簡介
在日常開發中,我們常會遇到「一個變數同時具備多個型別的屬性」的需求。傳統的 union type(聯集型別)只能描述「屬於 A 或 B」的情況,而無法表達「同時擁有 A 與 B」的資料結構。這時 Intersection Type(交集型別)就派上用場了。
交集型別讓我們可以把兩個或以上的型別「合併」成一個更嚴格的型別,只有同時符合所有組成型別的值才會通過編譯檢查。掌握這個概念,不僅能提升程式的型別安全性,還能在 React、Node.js、NestJS 等框架中寫出更具彈性且可維護的程式碼。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,一直到實務應用場景,帶你一步步深入了解 Intersection Type,適合 初學者到中級開發者 閱讀。
核心概念
1. 什麼是 Intersection Type?
在 TypeScript 中,交集型別使用 & 符號將多個型別結合。最簡單的形式是:
type A = { foo: string };
type B = { bar: number };
type C = A & B; // C 同時擁有 foo 與 bar
C 只接受同時具備 foo(字串)與 bar(數字)兩個屬性的物件。若缺少其中任何一個屬性,編譯器會報錯。
重點:交集型別是「所有」組成型別的集合,而非「任一」集合。
2. 基本語法與使用時機
| 場景 | 適合使用交集型別 |
|---|---|
| 需要把多個介面合併成一個完整的資料模型 | ✅ |
| 把函式參數的型別限制為同時符合多個條件 | ✅ |
想要在 React 的 props 中混合多個 HOC(Higher‑Order Component)所注入的屬性 |
✅ |
| 僅想表達「A 或 B」的關係 | ❌(應使用 Union) |
2.1 交集型別的基本寫法
type Person = { name: string };
type Employee = { employeeId: number };
type Staff = Person & Employee; // 必須同時有 name 與 employeeId
3. 交集型別與物件型別的結合
範例 1:合併兩個介面
interface Address {
city: string;
zip: string;
}
interface Contact {
phone: string;
email: string;
}
// 交集型別
type UserProfile = Address & Contact;
const user: UserProfile = {
city: "Taipei",
zip: "106",
phone: "02-1234-5678",
email: "example@domain.com",
};
UserProfile必須同時滿足Address與Contact兩個介面的結構。
範例 2:交叉函式參數
type WithTimestamp = { timestamp: Date };
type WithUser = { userId: string };
function logEvent(event: WithTimestamp & WithUser) {
console.log(`[${event.timestamp.toISOString()}] User ${event.userId} triggered an event`);
}
// 呼叫時必須同時提供兩個屬性
logEvent({ timestamp: new Date(), userId: "U12345" });
4. 交集型別與原始(Primitive)型別
雖然交集型別最常用於 物件型別,但它也可以與 字面量型別(Literal Types)結合,產生更精確的限制。
範例 3:字面量交集
type LiteralA = "A" | "B";
type LiteralB = "B" | "C";
type IntersectionLiteral = LiteralA & LiteralB; // 結果只剩 "B"
const value: IntersectionLiteral = "B"; // 正確
// const wrong: IntersectionLiteral = "A"; // 編譯錯誤
此例顯示交集會保留 共同的字面量,在 enum 或 狀態機 中非常實用。
5. 交集型別與泛型(Generics)
在泛型函式或類別中使用交集型別,可以動態地把多個型別合併,提升函式的彈性。
範例 4:泛型合併兩個型別
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const a = { id: 1, name: "Alice" };
const b = { age: 30, active: true };
const merged = merge(a, b);
// merged 的型別為 { id: number; name: string } & { age: number; active: boolean }
// => { id: 1, name: "Alice", age: 30, active: true }
透過 T & U,merge 能保證回傳值同時擁有兩個來源物件的所有屬性,且在編譯階段即完成型別推斷。
6. 交集型別與映射型別(Mapped Types)
結合 映射型別 可以快速產生「所有屬性都必填」的交集型別。
type PartialUser = {
name?: string;
age?: number;
};
type RequiredUser = {
[K in keyof PartialUser]-?: PartialUser[K];
};
// 等同於 { name: string; age: number; }
type FullUser = RequiredUser & { id: string };
const user2: FullUser = {
id: "U001",
name: "Bob",
age: 28,
};
這裡 RequiredUser 透過映射型別把所有屬性從 optional 變成 required,再與其他型別交集,形成更完整的模型。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
交集與 any/unknown |
若其中一個型別是 any,交集結果仍是 any;若是 unknown,交集會變成另一側的型別。 |
盡量避免在交集中使用 any,改用具體型別或 unknown 搭配型別守衛。 |
| 屬性衝突 | 當兩個型別有相同屬性但型別不同,交集會產生 交叉型別(intersection of property types),有時會得到 never。 |
確認合併的型別屬性相容,或使用 & 前先做型別調整(例如透過映射型別改為共用型別)。 |
| 過度交叉 | 把過多型別交叉在一起會讓型別變得過於嚴格,導致實作困難。 | 只在需求明確且必要時使用交集,過度交叉時考慮使用 interface extends 或 組合 的方式。 |
| 函式重載與交集 | 交集型別無法直接取代函式重載的多簽名情況。 | 若需要根據參數型別返回不同結果,仍建議使用 函式重載 或 條件型別。 |
| 與 Union 混用時的分配律 | `A & (B | C)會分配成(A & B) |
最佳實踐
- 保持交集的可讀性:若交集型別過長,考慮拆成多個小型別再組合,或使用
interface繼承。 - 使用
as const:在字面量交集時,配合as const讓 TypeScript 推斷出精確的字面量型別。 - 型別守衛:對於
unknown與交集混用的情況,寫型別守衛函式以保證屬性存在與型別正確。 - 文件化交集意圖:在程式碼註解或
/** */JSDoc 中說明為何需要交集,避免未來維護者產生誤解。
實際應用場景
1. React 高階組件(HOC)注入多個 Props
type WithTheme = { theme: "light" | "dark" };
type WithAuth = { userId: string };
function withTheme<P>(Component: React.ComponentType<P>) {
return (props: P) => <Component {...props} theme="light" />;
}
function withAuth<P>(Component: React.ComponentType<P>) {
return (props: P) => <Component {...props} userId="U123" />;
}
// 交集型別自動推斷最終 Props
const Enhanced = withTheme(withAuth(MyComponent));
// MyComponent 必須接受 theme 與 userId
type Props = React.ComponentProps<typeof Enhanced>;
Enhanced 的 Prop 型別自動變成 WithTheme & WithAuth & 原始 Props,讓開發者在使用時得到完整的 IntelliSense。
2. API 回傳資料的合併型別
假設後端分兩次回傳使用者基本資訊與授權資訊,我們可以在前端把兩個回應合併成一個完整的型別:
type BasicInfo = { id: string; name: string };
type PermissionInfo = { roles: string[]; token: string };
async function fetchUser(id: string): Promise<BasicInfo & PermissionInfo> {
const [basic, perm] = await Promise.all([
fetch(`/api/user/${id}`).then(r => r.json()),
fetch(`/api/perm/${id}`).then(r => r.json()),
]);
return { ...basic, ...perm };
}
使用 BasicInfo & PermissionInfo 保證最終物件同時擁有兩個來源的屬性,減少手動型別斷言的需求。
3. 資料驗證與型別守衛
type HasId = { id: number };
type HasName = { name: string };
function isHasId(obj: any): obj is HasId {
return typeof obj.id === "number";
}
function process(item: HasId & HasName) {
console.log(`ID: ${item.id}, Name: ${item.name}`);
}
// 透過型別守衛確保交集成立
function handle(raw: any) {
if (isHasId(raw) && "name" in raw) {
process(raw); // 此時 raw 被推斷為 HasId & HasName
}
}
結合 型別守衛 與交集型別,可以在執行時安全地檢查資料,同時在編譯時得到完整的型別資訊。
總結
- Intersection Type (
&) 是 TypeScript 中描述「同時具備多個型別」的核心工具,適用於物件、字面量、泛型等各種情境。 - 透過交集,我們可以 合併介面、強化函式參數、建立精確的資料模型,讓程式在編譯階段即捕捉到潛在的錯誤。
- 使用時需留意 屬性衝突、過度交叉、與
any/unknown的互動,並遵循 保持可讀性、使用型別守衛、適度抽象 的最佳實踐。 - 在 React HOC、API 合併回應、資料驗證 等實務場景中,交集型別能大幅提升型別安全與開發效率。
掌握了 Intersection Type,你將能在 TypeScript 項目中寫出更嚴謹、更具彈性的程式碼,為團隊的長期維護與擴充奠定堅實基礎。祝你在 TypeScript 的世界裡玩得開心、寫得順手! 🚀