本文 AI 產出,尚未審核

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. 基本語法與使用時機

場景 適合使用交集型別
需要把多個介面合併成一個完整的資料模型
把函式參數的型別限制為同時符合多個條件
想要在 Reactprops 中混合多個 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 必須同時滿足 AddressContact 兩個介面的結構。

範例 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 & Umerge 能保證回傳值同時擁有兩個來源物件的所有屬性,且在編譯階段即完成型別推斷。


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)

最佳實踐

  1. 保持交集的可讀性:若交集型別過長,考慮拆成多個小型別再組合,或使用 interface 繼承。
  2. 使用 as const:在字面量交集時,配合 as const 讓 TypeScript 推斷出精確的字面量型別。
  3. 型別守衛:對於 unknown 與交集混用的情況,寫型別守衛函式以保證屬性存在與型別正確。
  4. 文件化交集意圖:在程式碼註解或 /** */ 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 的世界裡玩得開心、寫得順手! 🚀