本文 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';
};

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

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

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

3. 基本語法與型別推論

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

// 泛型類別表達式,回傳一個帶有 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 的世界裡玩得開心,寫出更具彈性與可維護性的程式! 🚀