TypeScript 類別(Classes)── 存取子(Getter / Setter)
簡介
在物件導向程式設計中,類別是封裝資料與行為的核心概念。除了建構子(constructor)與普通方法之外,TypeScript 也支援 存取子(getter / setter),讓開發者能以屬性存取的語法,背後執行自訂的邏輯。
使用 getter / setter 可以:
- 保護內部資料:避免外部直接修改,加入驗證或轉換機制。
- 提升可讀性:呼叫方式看起來像屬性存取,語意更直觀。
- 維護相容性:在不改變 API 的前提下,逐步加入額外行為,減少破壞性變更。
對於從 JavaScript 轉向 TypeScript 的開發者來說,了解存取子的語法與最佳實踐,是寫出安全、易維護程式碼的關鍵一步。
核心概念
1. 基本語法
在 TypeScript 中,get 與 set 關鍵字分別用來定義取值與賦值的存取子。它們必須成對出現在同一個類別裡,且名稱相同。
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。 |
最佳實踐:
- 私有欄位命名統一:建議使用
_前綴或#(私有欄位語法)以明確區分。 - 驗證邏輯放在 setter:讓資料在進入類別前即完成檢查,保持 getter 的純粹性。
- 保持 getter 為「純函數」:不要在 getter 中改變其他狀態或產生副作用。
- 文件化存取子行為:在註解或 API 文件中說明 getter / setter 會執行哪些邏輯,避免使用者誤解。
- 考慮可序列化:若物件需要 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 更深層的功能! 🚀