本文 AI 產出,尚未審核

TypeScript 類別(Classes)── 存取子(Getter / Setter)

簡介

在物件導向程式設計中,類別是封裝資料與行為的核心概念。除了建構子(constructor)與普通方法之外,TypeScript 也支援 存取子(getter / setter),讓開發者能以屬性存取的語法,背後執行自訂的邏輯。

使用 getter / setter 可以:

  • 保護內部資料:避免外部直接修改,加入驗證或轉換機制。
  • 提升可讀性:呼叫方式看起來像屬性存取,語意更直觀。
  • 維護相容性:在不改變 API 的前提下,逐步加入額外行為,減少破壞性變更。

對於從 JavaScript 轉向 TypeScript 的開發者來說,了解存取子的語法與最佳實踐,是寫出安全、易維護程式碼的關鍵一步。


核心概念

1. 基本語法

在 TypeScript 中,getset 關鍵字分別用來定義取值賦值的存取子。它們必須成對出現在同一個類別裡,且名稱相同。

class Person {
  private _age: number = 0; // 真正儲存年齡的私有欄位

  // 取值(getter)
  get age(): number {
    return this._age;
  }

  // 賦值(setter)
  set age(value: number) {
    if (value < 0) {
      throw new Error('年齡不能為負數');
    }
    this._age = value;
  }
}
  • 取值子只能回傳值,不能接受參數。
  • 賦值子只能接受 單一 參數(即將要設定的值),且不能回傳值(void)。

2. 為什麼要使用私有欄位(_age)?

直接把資料公開會讓外部程式自由修改,失去封裝的好處。透過 私有欄位 (private) 搭配 存取子,我們可以在設定值前加入驗證、在取得值前做格式化,甚至在未來需求變更時不必改動 API。

3. 只寫 getter 或 setter?

根據需求,存取子可以只實作其中之一:

  • 只寫 getter:屬性變成唯讀(read‑only),外部只能讀取,無法賦值。
  • 只寫 setter:屬性只能寫入,常見於「寫入即觸發」的情境,例如日誌或事件系統。
class Config {
  private _mode: string = 'development';

  // 只讀屬性
  get mode(): string {
    return this._mode;
  }

  // 只寫屬性(用於觸發行為)
  set log(message: string) {
    console.log(`[Log] ${message}`);
  }
}

4. 使用 readonly 與存取子比較

TypeScript 的 readonly 修飾符可以讓屬性在編譯期變為唯讀,但 仍然可以直接存取,且不允許在執行時加入額外邏輯。若需要在讀取時做運算或格式化,仍須使用 getter。

class Point {
  // 編譯期唯讀,執行時直接存取
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  // 若想在取得時計算距離,就需要 getter
  get distanceFromOrigin(): number {
    return Math.sqrt(this.x ** 2 + this.y ** 2);
  }
}

5. 透過存取子實作「虛擬屬性」

有時候我們希望提供「看似」存在的屬性,但實際上是由其他欄位計算而來。這種 虛擬屬性 常用於 UI、資料轉換等場合。

class Rectangle {
  constructor(public width: number, public height: number) {}

  // 虛擬屬性:面積
  get area(): number {
    return this.width * this.height;
  }

  // 設定寬高時,同步更新面積(示範用,實務上較少這樣寫)
  set area(value: number) {
    const ratio = Math.sqrt(value / (this.width * this.height));
    this.width *= ratio;
    this.height *= ratio;
  }
}

程式碼範例

以下示範 4 個在日常開發中常見的 getter / setter 用法,並加入 註解說明,方便讀者快速掌握要點。

範例 1️⃣ 基本驗證(年齡)

class User {
  private _age: number = 0;

  /** 取得年齡 */
  get age(): number {
    return this._age;
  }

  /** 設定年齡,若小於 0 則拋出錯誤 */
  set age(value: number) {
    if (value < 0) {
      throw new Error('年齡不能為負數');
    }
    this._age = value;
  }
}

// 使用
const u = new User();
u.age = 25;          // ✅ 正常設定
console.log(u.age); // 25
// u.age = -5;       // ❌ 會拋出 Error

範例 2️⃣ 只讀屬性(設定檔模式)

class AppConfig {
  private _env: 'development' | 'production' = 'development';

  /** 只讀屬性,外部無法改變 */
  get env(): 'development' | 'production' {
    return this._env;
  }

  /** 內部方法可變更 */
  setEnvironment(env: 'development' | 'production'): void {
    this._env = env;
  }
}

// 使用
const cfg = new AppConfig();
console.log(cfg.env); // development
cfg.setEnvironment('production');
console.log(cfg.env); // production
// cfg.env = 'development'; // ❌ 編譯錯誤:Cannot assign to 'env' because it is a read‑only property.

範例 3️⃣ 觸發副作用(寫入即 log)

class Logger {
  private _messages: string[] = [];

  /** 寫入即印出訊息 */
  set log(message: string) {
    this._messages.push(message);
    console.log(`[Log] ${message}`);
  }

  /** 取得所有已記錄的訊息 */
  get history(): readonly string[] {
    return this._messages;
  }
}

// 使用
const logger = new Logger();
logger.log = '系統啟動'; // 立即在 console 印出
logger.log = '使用者登入';
console.log(logger.history); // ['系統啟動', '使用者登入']

範例 4️⃣ 虛擬屬性 + 格式化(日期)

class Event {
  private _timestamp: number = Date.now(); // 儲存毫秒數

  /** 取得 ISO 8601 格式的日期字串 */
  get isoDate(): string {
    return new Date(this._timestamp).toISOString();
  }

  /** 允許設定 ISO 8601 字串,內部自動轉為毫秒數 */
  set isoDate(value: string) {
    const date = new Date(value);
    if (isNaN(date.getTime())) {
      throw new Error('無效的日期字串');
    }
    this._timestamp = date.getTime();
  }
}

// 使用
const ev = new Event();
console.log(ev.isoDate); // 例如 "2025-11-19T08:30:00.000Z"
ev.isoDate = '2025-12-01T00:00:00Z';
console.log(ev.isoDate); // "2025-12-01T00:00:00.000Z"

常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記加 private 欄位 直接在 getter / setter 中使用 this.age 會產生遞迴呼叫,導致 Stack Overflow。 使用前綴(_#)或 private 欄位儲存實際值。
setter 無回傳值 有些開發者習慣在 setter 中 return this 以支援鏈式呼叫,但 TypeScript 規範不允許回傳值。 若需要鏈式,改用普通方法(setAge(value))或返回 this普通方法
過度使用 getter 每次存取都執行大量計算會影響效能。 僅在必要時使用,或將結果快取(memoization)。
在 getter 中拋出例外 讀取屬性時拋錯誤會讓使用者難以預測。 若需驗證,建議在 setter 或建構子驗證,getter 只回傳已驗證的值。
忽略 readonly 想要「唯讀」屬性卻只寫了 getter,實際上仍可在類別內部任意修改。 若屬性在類別外部不應變動,使用 private + getter,或在 public readonly 結合 getter。

最佳實踐

  1. 私有欄位命名統一:建議使用 _ 前綴或 #(私有欄位語法)以明確區分。
  2. 驗證邏輯放在 setter:讓資料在進入類別前即完成檢查,保持 getter 的純粹性。
  3. 保持 getter 為「純函數」:不要在 getter 中改變其他狀態或產生副作用。
  4. 文件化存取子行為:在註解或 API 文件中說明 getter / setter 會執行哪些邏輯,避免使用者誤解。
  5. 考慮可序列化:若物件需要 JSON.stringify,確保私有欄位可正確序列化或提供自訂 toJSON 方法。

實際應用場景

場景 為什麼適合使用 getter / setter
表單驗證 使用 setter 檢查使用者輸入,若不符合規則直接拋錯或自動修正;getter 可返回已格式化的值(如日期、金額)。
資料庫 ORM 欄位值在設定時自動轉型(字串 ↔ 數字、JSON ↔ 物件),讀取時自動轉成符合型別的值。
國際化(i18n) 透過 getter 依照當前語系返回對應文字;setter 可改變語系並觸發 UI 重新渲染。
懶加載(Lazy Loading) getter 第一次被呼叫時才載入資料或初始化物件,節省資源。
監控與日誌 setter 寫入敏感屬性時自動產生日誌,或在 getter 中記錄讀取次數以作分析。

小技巧:在 React、Vue 等前端框架中,使用 getter 來計算衍生狀態(computed properties)非常自然;而在 Node.js 後端,setter 常用於保護環境變數或配置檔的寫入。


總結

  • getter / setter 為 TypeScript 類別提供了屬性級別的封裝行為擴充,讓程式碼更具可讀性與安全性。
  • 正確的寫法是:私有欄位 + 公開存取子,並在 setter 中處理驗證與轉換,在 getter 中僅回傳已驗證的值。
  • 避免常見陷阱(遞迴、過度計算、未文件化行為),並遵循最佳實踐(命名統一、純函數 getter、文件說明),即可在專案中穩定運用。
  • 在實務開發中,從 表單驗證ORM國際化,存取子都是解耦與增強可維護性的好幫手。

掌握了 getter / setter 之後,你的 TypeScript 類別將不僅是資料容器,更是一個自我保護、可自我演化的智慧實體。祝你寫程式愉快,持續探索 TypeScript 更深層的功能! 🚀