本文 AI 產出,尚未審核

TypeScript – 物件與介面(Objects & Interfaces)

主題:多重介面繼承


簡介

在大型前端或 Node.js 專案中,介面(interface) 常被用來描述資料結構、函式簽名或類別的公共行為。隨著專案規模的擴大,單一介面往往無法完整涵蓋所有需求,此時 多重介面繼承(multiple interface inheritance)就成為一個強大且彈性的工具。透過同時繼承多個介面,我們可以把既有的抽象概念組合起來,形成更具表現力的型別,同時保持程式碼的可讀性與可維護性。

本篇文章將以 淺顯易懂 的方式說明多重介面繼承的語法、背後的型別系統原理,並提供 實務範例、常見陷阱與最佳實踐,幫助初學者到中級開發者快速上手並在真實專案中正確運用。


核心概念

1. 介面的基本繼承 – extends

在 TypeScript 中,介面可以使用 extends 關鍵字繼承另一個或多個介面。語法上只要在 extends 後列出欲繼承的介面名稱,並以逗號分隔即可。

interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: string;
}

重點Employee 同時擁有 Personnameage,以及自己的 employeeId

2. 多重介面繼承

若要一次繼承兩個以上的介面,只需要在 extends 後列出多個介面名稱:

interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(message: string): void;
}

/** 同時繼承 Serializable 與 Loggable */
interface DataModel extends Serializable, Loggable {
  id: number;
}

DataModel 會同時要求實作 serializelog 兩個方法,以及 id 屬性。

3. 類別實作多個介面

類別(class)可以 同時實作 多個介面,且每個介面都必須完整實作其所有成員。

class User implements Person, Loggable {
  constructor(public name: string, public age: number) {}

  log(message: string): void {
    console.log(`[${this.name}] ${message}`);
  }
}

此時 User 必須符合 Personnameage)以及 Loggablelog)的規範。

4. 交叉型別(Intersection Types) &

有時我們不想建立新介面,只想在型別層面「合併」多個介面,這時可以使用 交叉型別 (&):

type Auditable = Serializable & Loggable;

const audit: Auditable = {
  serialize() { return JSON.stringify(this); },
  log(message) { console.log(message); },
  // 其他屬性可自行加入
};

交叉型別的行為與多重介面繼承相同,只是它是 型別別名(type alias),不會產生實體的介面宣告。

5. 介面衝突解決

當多個介面定義了同名屬性或方法,但型別不相容時,TypeScript 會在編譯階段報錯。例如:

interface A { value: string; }
interface B { value: number; }

interface C extends A, B {}   // ❌ 錯誤:屬性 'value' 同時出現在 A 與 B 中,型別不相容

解決方式通常是 重新命名抽取共同屬性至基礎介面,或 使用聯合型別 (|) 以允許多種型別。


程式碼範例

以下提供 五個實用範例,說明多重介面繼承的不同應用情境。

範例 1️⃣:組合 UI Props

interface Clickable {
  onClick: (e: MouseEvent) => void;
}

interface Focusable {
  onFocus: () => void;
}

/** ButtonProps 同時具備 Clickable 與 Focusable 的屬性 */
interface ButtonProps extends Clickable, Focusable {
  label: string;
}

/* 使用範例 */
const btn: ButtonProps = {
  label: "送出",
  onClick(e) { console.log("點擊", e); },
  onFocus() { console.log("取得焦點"); }
};

實務意義:在 React、Vue 等框架中,組合多個「行為」介面可以快速產生完整的 component props。


範例 2️⃣:類別實作多個介面(服務層)

interface Cacheable {
  getFromCache(key: string): any;
  setToCache(key: string, value: any): void;
}

interface RemoteFetchable {
  fetchFromServer(endpoint: string): Promise<any>;
}

/** 服務類別同時具備快取與遠端抓取的能力 */
class DataService implements Cacheable, RemoteFetchable {
  private cache = new Map<string, any>();

  getFromCache(key: string) { return this.cache.get(key); }

  setToCache(key: string, value: any) { this.cache.set(key, value); }

  async fetchFromServer(endpoint: string) {
    const response = await fetch(endpoint);
    const data = await response.json();
    this.setToCache(endpoint, data);
    return data;
  }
}

關鍵:透過「介面」劃分職責(Cache、Remote),類別只需關注自己的實作,日後若要替換快取機制,只需要改寫 Cacheable 相關類別即可。


範例 3️⃣:交叉型別建構複雜型別

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface Identifiable {
  id: string;
}

/** 使用交叉型別組合兩個介面 */
type Entity = Timestamped & Identifiable;

const user: Entity = {
  id: "u123",
  createdAt: new Date(),
  updatedAt: new Date(),
};

說明Entity 並未宣告為新介面,而是直接以 type 建立,適合在函式簽名或臨時型別中使用。


範例 4️⃣:混入(Mixin)與多重介面

type Constructor<T = {}> = new (...args: any[]) => T;

/** 可混入的功能介面 */
interface Draggable {
  dragStart(): void;
  dragEnd(): void;
}

/** Mixin 函式 */
function DraggableMixin<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements Draggable {
    dragStart() { console.log("開始拖曳"); }
    dragEnd()   { console.log("結束拖曳"); }
  };
}

/** 基礎類別 */
class Widget {
  constructor(public name: string) {}
}

/** 產生同時具備 Draggable 的新類別 */
class DraggableWidget extends DraggableMixin(Widget) {}

const w = new DraggableWidget("圖表");
w.dragStart();   // => 開始拖曳
w.dragEnd();     // => 結束拖曳

實務應用:在 UI 框架中,透過 Mixin 可以把「可拖曳」或「可縮放」等行為彈性加入任意類別,而不需要多層繼承。


範例 5️⃣:介面衝突的解決方式

interface Readable {
  value: string;
}
interface Writable {
  value: number;   // 與 Readable 的型別衝突
}

/* 方案 1:抽取共同屬性 */
interface Base {
  value: string | number;
}
interface Readable extends Base {}
interface Writable extends Base {}

interface Combined extends Readable, Writable {
  // 現在 value 允許 string 或 number
}

/* 方案 2:使用聯合型別 */
type Flexible = Readable | Writable;

const a: Flexible = { value: "hello" }; // OK
const b: Flexible = { value: 42 };      // OK

技巧:當必須同時支援不同型別時,抽取共同基底使用聯合型別是最安全的做法。


常見陷阱與最佳實踐

陷阱 可能的後果 建議的解決方式
屬性名稱衝突且型別不相容 編譯錯誤、難以維護 抽取共同基底介面、使用聯合或交叉型別、或重新命名屬性
過度繼承導致介面龐大 失去介面的可讀性,影響 IntelliSense 效能 介面保持單一職責(SRP),必要時拆成小介面再組合
忘記在類別中實作所有介面成員 編譯失敗,或在執行時出現 undefined 錯誤 使用 implements 時,讓 IDE 自動提示缺漏成員
交叉型別與聯合型別混用不當 型別推斷變得模糊,導致意外的 any 明確區分「必須同時具備」(&) 與「任一」(`
循環依賴的介面 編譯時無法解析,可能產生 Maximum call stack size exceeded 重新設計介面結構,或使用 type 別名搭配前置聲明

最佳實踐

  1. 介面保持小而專:每個介面只描述一組相關的行為或屬性,組合時再使用 extends 或交叉型別。
  2. 優先使用交叉型別:當只需要臨時合併型別時,type A = B & C 更簡潔。
  3. 命名規則:介面名稱以 I 開頭(如 IUser)或使用描述性名詞(如 Serializable),保持一致性。
  4. 避免循環依賴:若兩個介面互相引用,考慮抽出共同部分或改用 type 別名。
  5. 利用 readonly?:在多重繼承的情況下,使用 readonly 防止意外改寫,使用可選屬性 (?) 提供彈性。

實際應用場景

場景 為何需要多重介面繼承 範例
API 回應模型 一個回應可能同時具備分頁資訊、錯誤代碼與實體資料 interface Paginated extends PageInfo, ErrorInfo { data: T[]; }
React Component Props 按鈕需要同時支援點擊、鍵盤、懸停等行為 interface ButtonProps extends Clickable, Keyboardable, Hoverable { label: string; }
Domain Entity 企業系統中,實體往往同時是可追蹤 (Timestamped) 且可辨識 (Identifiable) type Entity = Timestamped & Identifiable;
微服務間的契約 服務間傳遞的訊息可能同時包含驗證資訊與加密資訊 interface Message extends Verifiable, Encryptable { payload: any; }
混入式功能擴充 為既有類別動態加入「可拖曳」或「可縮放」等 UI 行為 class DraggableWidget extends DraggableMixin(Widget) {}

透過多重介面繼承,我們能夠 在型別層面組合職責,同時保持程式碼的 可讀性可測試性,這在大型團隊開發與長期維護中尤其重要。


總結

  • 多重介面繼承是 TypeScript 型別系統裡 組合抽象 的核心工具。
  • 使用 extends 可以一次繼承多個介面,類別則可透過 implements 同時實作多個介面。
  • 交叉型別 (&) 提供了 不建立實體介面 的快速合併方式,適合臨時或函式型別。
  • 必須留意 屬性衝突介面過大循環依賴 等常見陷阱,並遵循 單一職責明確命名適度使用 readonly 的最佳實踐。
  • 在 UI 組件、API 合約、服務層與混入式功能等真實場景中,多重介面繼承能顯著提升程式碼的彈性與可維護性。

掌握了這些概念後,你就可以在 TypeScript 專案裡 以「組合」取代「繼承」 的方式,寫出更乾淨、更安全的程式碼。祝你在日常開發中玩得開心,寫出高品質的 TypeScript 程式!