本文 AI 產出,尚未審核

TypeScript – 類別(Classes)

主題:constructor 型別宣告


簡介

在 TypeScript 中,類別(class) 不只是 ES6 的語法糖,它還提供了強大的型別系統支援。constructor 作為類別最重要的入口點,負責建立物件、注入相依性以及初始化屬性。若能正確為 constructor 宣告型別,不僅能在編譯期即捕捉錯誤,還能提升 IDE 的自動完成與文件提示,讓程式碼的可讀性與可維護性大幅提升。

本篇文章將從 基本語法參數屬性簡寫重載(overload)this 型別 以及 泛型建構子 等多個面向,深入說明 constructor 的型別宣告方式,並提供實務範例、常見陷阱與最佳實踐,幫助讀者在日常開發中正確運用這項功能。


核心概念

1. 基本語法與參數型別

在 TypeScript 中,constructor 的參數必須明確標註型別,否則會被推斷為 any,失去型別安全的好處。

class Person {
  name: string;
  age: number;

  // 1️⃣ 明確宣告參數型別
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

重點:若忘記寫型別,tsc 會警告 "Parameter 'name' implicitly has an 'any' type",建議在 tsconfig.json 中開啟 "noImplicitAny": true


2. 參數屬性簡寫(Parameter Properties)

TypeScript 允許在 constructor 參數前加上存取修飾子(publicprivateprotectedreadonly),自動產生對應的屬性並完成賦值,省去繁瑣的 this.xxx = xxx 程式碼。

class Car {
  // 2️⃣ 透過參數屬性一次完成宣告與賦值
  constructor(public brand: string, private year: number) {}

  getInfo(): string {
    // year 為 private,只能在類別內部存取
    return `${this.brand} (${this.year})`;
  }
}

此寫法的等價展開如下:

class Car {
  public brand: string;
  private year: number;

  constructor(brand: string, year: number) {
    this.brand = brand;
    this.year = year;
  }
}

技巧:若屬性需要 readonly,直接加上 readonly 修飾子即可,編譯後會產生 private readonly 成員。


3. 建構子重載(Constructor Overload)

有時候同一個類別需要接受不同形態的參數組合,這時可以使用 重載 來描述多種簽名,實作只保留一個 constructor

class Point {
  x: number;
  y: number;

  // 3️⃣ 重載簽名
  constructor(x: number, y: number);
  constructor(coord: [number, number]);
  constructor(xOrCoord: number | [number, number], y?: number) {
    if (Array.isArray(xOrCoord)) {
      [this.x, this.y] = xOrCoord;
    } else {
      this.x = xOrCoord;
      this.y = y!;
    }
  }

  toString(): string {
    return `(${this.x}, ${this.y})`;
  }
}

使用時,IDE 會根據重載簽名提供正確的提示:

const p1 = new Point(10, 20);      // 兩個數字
const p2 = new Point([5, 15]);    // 坐標陣列

注意:重載只能寫在 宣告區(簽名)與 實作區(唯一的 constructor)兩段,且實作區的參數型別必須能覆蓋所有簽名。


4. this 型別的精確化

在類別方法中使用 this 時,TypeScript 會自動推斷其型別為該類別本身。但在 繼承混入(mixin) 情境下,this 可能指向子類別,這時可以使用 this 型別 讓方法返回子類別的正確型別。

class Builder {
  protected props: Record<string, any> = {};

  // 4️⃣ 以 this 為回傳型別,支援鏈式呼叫
  set(key: string, value: any): this {
    this.props[key] = value;
    return this;
  }

  build(): Record<string, any> {
    return this.props;
  }
}

class UserBuilder extends Builder {
  setName(name: string): this {
    return this.set('name', name);
  }

  setAge(age: number): this {
    return this.set('age', age);
  }
}

const user = new UserBuilder()
  .setName('Alice')
  .setAge(30)
  .build();   // user 為 { name: 'Alice', age: 30 }

此寫法讓 setNamesetAge 可正確回傳 UserBuilder,而非基底 Builder,避免鏈式呼叫時出現型別錯誤。


5. 泛型建構子(Generic Constructor)

有時候類別本身需要根據外部型別參數動態決定屬性型別,這時可以讓 constructor 變成 泛型

class Repository<T> {
  private items: T[] = [];

  // 5️⃣ 泛型建構子:接受任意型別的初始陣列
  constructor(initialItems: T[] = []) {
    this.items = initialItems;
  }

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return [...this.items];
  }
}

// 使用範例
interface User {
  id: number;
  name: string;
}
const userRepo = new Repository<User>([{ id: 1, name: 'Bob' }]);
userRepo.add({ id: 2, name: 'Carol' });

在上述例子中,Repositoryconstructor 會根據 T 的實際型別自動推斷 initialItems 的型別,使得 編譯期即保證 陣列內容的正確性。


常見陷阱與最佳實踐

陷阱 說明 解法 / 建議
忘記宣告參數型別 會導致 any,失去型別安全。 開啟 noImplicitAny,或使用 strict 模式。
重載實作不符合所有簽名 會出現「類別建構子實作與重載不相容」錯誤。 確保實作參數型別能接受所有重載的可能值,必要時使用聯合型別或類型守衛。
constructor 中直接使用 this 之前未初始化屬性 this 讀取未賦值屬性會報錯。 先在屬性宣告時提供預設值,或在 constructor 前寫 !(非空斷言)※慎用。
過度使用 anyunknown 失去型別檢查的好處。 儘量使用具體型別或泛型,只有在確實無法預測時才使用 unknown,再透過型別守衛縮窄。
混用 publicprivate 參數屬性與手動賦值 可能產生重複宣告或未預期行為。 選擇其一:若使用參數屬性,則不需要在建構子內再寫 this.xxx = xxx;

最佳實踐

  1. 始終為 constructor 參數加上型別,即使是簡單的 stringnumber
  2. 善用參數屬性 簡化程式碼,同時保留存取修飾子以控制可見性。
  3. 在需要多樣化建構方式時,使用 重載 讓呼叫端得到正確的型別提示。
  4. 若類別支援鏈式呼叫,使用 this 型別返回自身,提升可讀性與安全性。
  5. 對於資料容器或服務層,考慮使用 泛型建構子,讓類別在不同情境下保持彈性。

實際應用場景

  1. API 客戶端

    class ApiClient {
      private baseUrl: string;
      private token?: string;
    
      constructor(baseUrl: string, token?: string) {
        this.baseUrl = baseUrl;
        this.token = token;
      }
    
      // 依據 token 是否提供,動態調整 Header
    }
    

    透過嚴格的 constructor 型別,開發者在建立 ApiClient 時必須提供正確的 URL,若忘記傳入 token,編譯器會提示缺少參數。

  2. 表單驗證器(Validator)
    使用 參數屬性 搭配 泛型,一次完成屬性宣告與注入驗證規則:

    class Validator<T> {
      constructor(public schema: Partial<Record<keyof T, (value: any) => boolean>>) {}
    }
    
  3. 資料庫模型(ORM)
    透過 建構子重載 支援「以 ID 讀取」或「以完整資料建立」兩種方式:

    class User {
      id: number;
      name: string;
      constructor(id: number);
      constructor(data: { id: number; name: string });
      constructor(arg: number | { id: number; name: string }) {
        if (typeof arg === 'number') {
          // 從 DB 讀取
        } else {
          this.id = arg.id;
          this.name = arg.name;
        }
      }
    }
    
  4. 插件系統(Plugin System)
    利用 this 型別 讓插件基底類別返回子類別,使得插件開發者可安全使用鏈式 API:

    abstract class PluginBase {
      protected config: Record<string, any> = {};
      setOption(key: string, value: any): this {
        this.config[key] = value;
        return this;
      }
    }
    

總結

  • constructor 是類別最關鍵的入口,正確的型別宣告 能在編譯期捕捉錯誤、提升 IDE 體驗。
  • 透過 參數屬性重載this 型別 以及 泛型建構子,我們可以寫出 簡潔、彈性且安全 的類別程式碼。
  • 常見的陷阱多與 型別遺漏重載不相容未初始化屬性 有關,遵守 嚴格模式、使用 型別守衛、避免過度使用 any,即可減少這些問題。

掌握了 constructor 的型別宣告技巧後,你將能在大型專案中更自信地設計 API、封裝商業邏輯,並在團隊協作時提供更清晰的契約(contract)。快把本文的範例搬進你的程式碼庫,讓 TypeScript 為你的類別保駕護航吧!