本文 AI 產出,尚未審核

TypeScript 物件與介面:介面繼承(extends)

簡介

在大型前端專案或 Node.js 後端服務中,型別安全是維持程式碼可讀性與維護性的關鍵。TypeScript 透過 interface 讓開發者可以為物件、函式甚至類別定義結構,而 介面繼承extends)則提供了 重用與擴充 型別的能力。

透過介面繼承,我們可以:

  1. 抽象共同屬性,避免重複宣告。
  2. 建立層級式的型別,讓程式碼更具語意。
  3. 在不同模組間共享合約,提升團隊協作效率。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 TypeScript 介面繼承的使用方式,幫助初學者快速上手,也讓中級開發者在實務上得到更多靈感。

核心概念

1. 基本語法

interface 可以像類別一樣使用 extends 來繼承另一個介面。被繼承的介面稱為 基礎介面(base interface),而繼承它的介面稱為 衍生介面(derived interface)。

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

/* PersonInfo 繼承 Person,並額外加入 address 屬性 */
interface PersonInfo extends Person {
  address: string;
}

此時 PersonInfo 同時擁有 nameageaddress 三個屬性,使用上等同於一次宣告三個欄位。

2. 多重繼承

TypeScript 允許介面同時繼承多個基礎介面,形成 交叉型別(intersection type)的效果。

interface Identifiable {
  id: string;
}
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

/* 同時繼承 Identifiable 與 Timestamped */
interface BlogPost extends Identifiable, Timestamped {
  title: string;
  content: string;
}

BlogPost 會擁有 idcreatedAtupdatedAttitlecontent 五個屬性。

3. 介面繼承類別

介面不僅能繼承其他介面,也可以 繼承類別的公有與受保護成員。這在需要把類別的結構抽象為型別時非常有用。

class Animal {
  protected species: string;
  constructor(species: string) {
    this.species = species;
  }
  public move(distance: number): void {
    console.log(`${this.species} moved ${distance}m`);
  }
}

/* DogInterface 繼承 Animal 的公有與受保護成員 */
interface DogInterface extends Animal {
  bark(): void;
}

/* 實作 DogInterface 的類別 */
class Dog extends Animal implements DogInterface {
  bark() {
    console.log('Woof!');
  }
}

DogInterface 包含了 species(受保護)與 move(公有)兩個成員,實作類別時直接繼承 Animal 即可滿足介面的要求。

4. 可選屬性與只讀屬性在繼承中的行為

  • 可選屬性?)在衍生介面中仍保持可選,除非在衍生介面重新宣告為必填。
  • 只讀屬性readonly)會沿襲至衍生介面,且不可在衍生介面中改為可寫。
interface Base {
  readonly id: number;
  name?: string;
}

/* 重新宣告 name 為必填,id 仍為 readonly */
interface Derived extends Base {
  name: string;   // 從可選變為必填
}

5. 介面與型別別名(type)的差異

雖然 type 也支援交叉 (&) 與擴展 (extends) 的概念,但 介面在宣告合併(declaration merging)上更具彈性。如果需要在多個檔案中擴充同一個型別,使用 interface 會比較直觀。

// fileA.ts
export interface Config {
  host: string;
}

// fileB.ts
import { Config } from './fileA';
export interface Config {
  port: number;   // 自動合併成 { host: string; port: number; }
}

程式碼範例

範例 1:基本繼承與物件驗證

interface User {
  username: string;
  email: string;
}

/* 讓 Admin 繼承 User,並加入權限屬性 */
interface Admin extends User {
  /** 管理員等級,1 為最低,5 為最高 */
  level: 1 | 2 | 3 | 4 | 5;
}

/* 正確使用範例 */
const superAdmin: Admin = {
  username: 'alice',
  email: 'alice@example.com',
  level: 5,
};

/* 錯誤示範:缺少 level 會在編譯時報錯 */
const invalidAdmin: Admin = {
  username: 'bob',
  email: 'bob@example.com',
  // ❌ Property 'level' is missing
};

範例 2:多重繼承與交叉型別

interface HasId {
  id: string;
}
interface HasMeta {
  createdBy: string;
  updatedBy?: string; // 可選
}

/* 結合兩個介面,形成完整的資料模型 */
interface Article extends HasId, HasMeta {
  title: string;
  content: string;
}

/* 實例 */
const article: Article = {
  id: 'a1b2c3',
  createdBy: 'admin',
  title: 'TypeScript 介面繼承教學',
  content: '...',
  // updatedBy 可以省略
};

範例 3:介面繼承類別與抽象類別的結合

abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;
}

/* 讓 CircleInterface 繼承 Shape,保留抽象方法 */
interface CircleInterface extends Shape {
  radius: number;
}

/* 實作 Circle,同時滿足介面與抽象類別的要求 */
class Circle extends Shape implements CircleInterface {
  constructor(public color: string, public radius: number) {
    super(color);
  }
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

/* 使用 */
const c = new Circle('red', 10);
console.log(`Area: ${c.area()}, Color: ${c.color}`);

範例 4:可選屬性變必填與只讀屬性保護

interface BaseConfig {
  readonly version: number;
  debug?: boolean;
}

/* 在衍生介面中把 debug 變為必填,version 仍為 readonly */
interface FullConfig extends BaseConfig {
  debug: boolean; // 必填
}

/* 正確使用 */
const cfg: FullConfig = {
  version: 1,
  debug: true,
};

/* 嘗試修改 readonly 屬性會錯誤 */
cfg.version = 2; // ❌ Cannot assign to 'version' because it is a read-only property.

範例 5:介面合併(Declaration Merging)實務應用

// lib.d.ts
export interface HttpResponse {
  status: number;
  data: any;
}

// app.ts
import { HttpResponse } from './lib';

/* 透過宣告合併為回傳加入 header */
export interface HttpResponse {
  headers: Record<string, string>;
}

/* 現在 HttpResponse 同時擁有 status、data、headers */
const resp: HttpResponse = {
  status: 200,
  data: { message: 'OK' },
  headers: { 'content-type': 'application/json' },
};

常見陷阱與最佳實踐

常見陷阱 說明 解決方式
忘記 extends 時的屬性遺失 直接在衍生介面中重新宣告相同屬性,可能會造成屬性不一致。 使用 介面繼承 而非重複宣告,讓 TypeScript 自動檢查一致性。
多重繼承衝突 兩個基礎介面有同名屬性且型別不相容,會產生錯誤。 盡量避免在不同介面中定義相同名稱的屬性,或使用 交叉型別 (&) 重新組合。
只讀屬性被意外改寫 在衍生介面或實作類別中忘記 readonly,編譯器不會警告。 始終保留 基礎介面的 readonly 修飾,必要時在衍生介面中再次宣告 readonly
介面繼承類別時的建構子 介面無法繼承類別的建構子,可能導致實作類別忘記傳遞參數。 在介面中只繼承 實例屬性與方法,建構子需求仍需在類別中自行處理。
過度繼承造成層級過深 多層繼承會讓型別圖變得難以追蹤。 適度使用 extends,必要時改用 型別別名的交叉 (type A = B & C) 來保持扁平結構。

最佳實踐

  1. 用介面描述公開合約:API 回傳、函式參數等應盡量使用 interface,讓其他模組只依賴合約而非實作。
  2. 保持單一職責:每個介面只描述一組相關屬性,透過 extends 組合成更完整的型別。
  3. 善用宣告合併:當第三方套件的型別需要擴充時,使用介面合併而非修改原始檔案。
  4. 配合 readonly?:在基礎介面中先設為只讀與可選,讓衍生介面根據需求提升約束。
  5. 測試型別:利用 // @ts-expect-erroras const 在單元測試中驗證介面的相容性,避免未預期的型別變更。

實際應用場景

場景 為何使用介面繼承 範例
REST API 回傳模型 不同端點共用 idcreatedAt,但各自有專屬欄位。 interface BaseEntity { id: string; createdAt: Date; }
interface User extends BaseEntity { username: string; }
Redux 狀態樹 基礎狀態 (loading, error) 於所有 slice 中共用。 interface SliceState { loading: boolean; error?: string; }
interface TodoState extends SliceState { items: Todo[]; }
第三方套件擴充 想在 express.Request 加入自訂屬性。 declare module 'express-serve-static-core' { interface Request { user?: User; } }
UI 元件屬性 基本屬性 (className, style) 被多個元件共用。 interface BaseProps { className?: string; style?: React.CSSProperties; }
interface ButtonProps extends BaseProps { onClick: () => void; }
Domain Model 繼承 企業領域模型中,Employee 繼承 Person,再加上 department interface Person { name: string; age: number; }
interface Employee extends Person { department: string; }

總結

  • 介面繼承 (extends) 是 TypeScript 中強大的型別重用機制,能讓我們在保持 型別安全 的同時,寫出更具可讀性與維護性的程式碼。
  • 透過 單一繼承、多人繼承、介面合併,我們可以彈性組合屬性、方法與只讀/可選限制,滿足各種實務需求。
  • 注意 屬性衝突、只讀保護與過度層級 等常見陷阱,並遵循 單一職責、介面作為合約 的最佳實踐,能讓專案在長期演進中保持穩定。

掌握了介面繼承後,你就能在大型 TypeScript 專案中,像搭積木一樣組合型別,快速建構出清晰、可擴充的程式架構。祝你在 TypeScript 的世界裡寫出更安全、更好維護的程式碼!