本文 AI 產出,尚未審核

TypeScript 類別表達式(Class Expression)教學


簡介

在 ES6 之後,JavaScript(以及 TypeScript)正式支援 類別(class) 語法,讓我們可以用更貼近物件導向的方式撰寫程式。大部分教材都會先介紹 類別宣告(class declaration),例如 class Person { … }。然而,類別表達式(class expression) 也是一個極具彈性的寫法,它允許我們把類別當作值(value)傳遞、立即執行或動態建立。

掌握類別表達式的概念,對於以下情境非常有幫助:

  1. 需要在函式內部動態產生類別(例如根據參數決定繼承層級)。
  2. 將類別作為高階函式的參數或回傳值,實現「類別工廠」模式。
  3. 在模組化開發中,避免全域污染,只在需要的範圍內保留類別定義。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你熟悉 TypeScript 中的類別表達式,並提供實務上可直接套用的案例。


核心概念

1. 什麼是類別表達式?

類別表達式與函式表達式的概念相同:把類別本身視為一個值,可以賦給變數、作為參數傳遞,甚至立即執行(IIFE)。語法上分為 具名(named)匿名(anonymous) 兩種形式:

// 匿名類別表達式
const MyClass = class {
  greet() {
    console.log('Hello');
  }
};

// 具名類別表達式(名稱僅在內部作用域可見)
const AnotherClass = class NamedClass {
  // 在類別內部可以透過 NamedClass 參照自己
  static className = 'NamedClass';
};

重點:類別表達式的名稱(若有)只在類別本身的內部作用域可見,外部只能透過變數(如 MyClassAnotherClass)存取。

2. 為什麼要使用類別表達式?

使用情境 好處
動態繼承(根據參數決定父類別) 只在需要時才產生子類別,避免不必要的類別宣告
類別工廠(返回類別的函式) 把類別建立的邏輯封裝,提升可重用性
立即執行的類別(IIFE) 可以在建立同時執行一次性初始化程式碼
防止命名衝突 把類別限制在局部變數中,減少全域污染

3. 基本語法與型別推論

在 TypeScript 中,類別表達式同樣支援 屬性修飾子publicprivateprotectedreadonly)以及 泛型。以下範例展示如何結合這些特性:

// 泛型類別表達式,回傳一個帶有 type 屬性的類別
function createWrapper<T>(defaultValue: T) {
  return class Wrapper {
    private _value: T = defaultValue;

    get value(): T {
      return this._value;
    }
    set value(v: T) {
      this._value = v;
    }
  };
}

// 使用
const NumberWrapper = createWrapper<number>(0);
const w = new NumberWrapper();
w.value = 42; // 正確,型別被推斷為 number

4. 類別表達式與 extends 的結合

下面示範 動態繼承:根據傳入的基底類別,回傳一個擴充功能的子類別。

class Animal {
  constructor(public name: string) {}
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

// 動態產生繼承自任意基底類別的子類別
function withLogging<T extends new (...args: any[]) => {}>(Base: T) {
  return class extends Base {
    // 新增一個記錄建構子參數的屬性
    private _log: string[] = [];

    constructor(...args: any[]) {
      super(...args);
      this._log.push(`Created ${Base.name} with args: ${JSON.stringify(args)}`);
    }

    get log() {
      return this._log;
    }

    // 覆寫 speak 方法,加入日誌
    speak() {
      this._log.push(`${this.constructor.name}.speak() called`);
      // 呼叫父類別的原始實作
      if (super.speak) super.speak();
    }
  };
}

// 建立帶有日誌功能的 Animal 子類別
const LoggedAnimal = withLogging(Animal);
const dog = new LoggedAnimal('Buddy');
dog.speak(); // Buddy makes a sound.
console.log(dog.log); // 查看日誌

5. 類別表達式的即時執行(IIFE)

有時候我們想在類別建立時就執行一次性的設定,例如註冊到全域容器或初始化靜態屬性。使用 IIFE 可以做到:

const Singleton = (function () {
  // 私有變數,外部無法直接存取
  let instance: Singleton | null = null;

  // 類別表達式
  return class Singleton {
    private constructor(public readonly id: number) {}

    static getInstance() {
      if (!instance) {
        instance = new Singleton(Date.now());
      }
      return instance;
    }

    greet() {
      console.log(`Singleton #${this.id} greets you!`);
    }
  };
})();

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
s1.greet();

常見陷阱與最佳實踐

陷阱 說明 建議的做法
名稱作用域混淆 具名類別表達式的名稱只在類別內部有效,外部無法存取。若誤以為外部可以使用會產生 ReferenceError 使用匿名類別或將名稱存入變數,避免在外部直接引用具名類別。
過度動態 動態產生大量類別會影響程式的可讀性與除錯體驗。 僅在需要動態繼承工廠時使用,平常仍以宣告式為主。
繼承鏈過深 使用多層類別表達式產生的繼承鏈,可能導致 instanceof 判斷不易理解。 在文件或註解中明確說明每層的目的,或使用介面(interface)取代過度繼承。
靜態屬性初始化 靜態屬性若在類別表達式內部使用 this,可能因為類別尚未完成宣告而出錯。 靜態屬性應直接賦值或使用立即執行的函式返回值。
型別推論失敗 在泛型類別表達式中,若未明確指定型別參數,TS 可能推斷為 any 使用 as const、明確的型別參數或 extends 限制來保證型別安全。

最佳實踐

  1. 保持簡潔:類別表達式的主要價值在於「動態」與「局部」的需求,若不需要動態性,就使用普通的類別宣告。
  2. 使用 readonly:對於只在建構子設定後不變的屬性,使用 readonly 可以提升可讀性與安全性。
  3. 文件化:在函式或模組的 JSDoc 中說明返回的類別結構與用途,方便其他開發者使用。
  4. 測試:對於透過類別表達式產生的類別,建議寫單元測試驗證其行為,特別是繼承與靜態屬性部分。

實際應用場景

1. UI 元件的動態子類別

在前端框架(如 React、Angular)中,我們常需要根據不同的配置產生特化的 UI 元件。使用類別表達式可以把「共用基底」與「客製化行為」分離:

// 基底元件
class Button {
  constructor(public label: string) {}
  render() {
    console.log(`Button: ${this.label}`);
  }
}

// 動態產生帶有樣式的子類別
function styledButton(style: 'primary' | 'secondary') {
  return class extends Button {
    render() {
      console.log(`[${style.toUpperCase()}] Button: ${this.label}`);
    }
  };
}

const PrimaryButton = styledButton('primary');
const btn = new PrimaryButton('送出');
btn.render(); // [PRIMARY] Button: 送出

2. API 客戶端的版本化

當後端提供多個 API 版本時,我們可以用類別表達式根據版本號返回對應的客戶端類別:

interface ApiClient {
  get<T>(url: string): Promise<T>;
}

// 版本 1
class ClientV1 implements ApiClient {
  async get<T>(url: string): Promise<T> {
    // ...實作
    return fetch(url).then(r => r.json());
  }
}

// 版本 2(加入認證頭)
class ClientV2 implements ApiClient {
  constructor(private token: string) {}
  async get<T>(url: string): Promise<T> {
    return fetch(url, {
      headers: { Authorization: `Bearer ${this.token}` },
    }).then(r => r.json());
  }
}

// 工廠函式
function createApiClient(version: 1 | 2, token?: string) {
  return version === 1
    ? class extends ClientV1 {}
    : class extends ClientV2 {
        constructor() {
          super(token!);
        }
      };
}

// 使用
const ApiClient = createApiClient(2, 'abc123');
const api = new ApiClient();
api.get('/users').then(console.log);

3. 依賴注入(DI)容器的註冊

在大型 Node.js 專案中,我們常使用 DI 容器管理服務。類別表達式可以在註冊時即完成設定:

type ServiceConstructor<T> = new (...args: any[]) => T;

class Container {
  private map = new Map<string, any>();

  register<T>(key: string, ctor: ServiceConstructor<T>) {
    this.map.set(key, new ctor());
  }

  resolve<T>(key: string): T {
    return this.map.get(key);
  }
}

// 動態產生具備日誌功能的服務類別
function withLogger<T extends ServiceConstructor<any>>(Base: T) {
  return class extends Base {
    log(message: string) {
      console.log(`[${Base.name}] ${message}`);
    }
  };
}

// 註冊
const container = new Container();
container.register('userService', withLogger(class {
  getUser(id: number) {
    return { id, name: 'Alice' };
  }
}));

const userService = container.resolve<any>('userService');
userService.log('Fetching user...');
console.log(userService.getUser(1));

總結

類別表達式是 TypeScript 中一項 靈活且功能強大的語法,它讓我們可以:

  • 把類別當作第一級公民,在變數、參數、回傳值之間自由傳遞。
  • 根據執行時資訊動態產生子類別,解決傳統類別宣告無法因條件改變繼承的限制。
  • 封裝類別建立邏輯,實作工廠模式、單例模式或依賴注入的自動註冊。

在實務開發中,適度使用類別表達式可以提升程式碼的 可組合性可測試性,同時避免全域命名衝突。記得遵守最佳實踐:保持簡潔、加上完整註解、針對動態產生的類別撰寫測試,才能讓這項技巧真正為你的專案增值。

祝你在 TypeScript 的世界裡玩得開心,寫出更具彈性與可維護性的程式! 🚀