本文 AI 產出,尚未審核

TypeScript 物件與介面(Objects & Interfaces)

主題:函式屬性(方法定義)


簡介

在 JavaScript 中,函式本身就是一等公民,物件可以直接把函式當成屬性儲存,形成「方法」。
進入 TypeScript 後,我們不僅可以對資料屬性加上型別,對函式屬性也能寫得更精確,讓編譯器在開發階段即捕捉錯誤、提供自動補完,提升程式的可讀性與維護性。

本篇文章將從 介面的函式屬性宣告this 型別可選與只讀重載與泛型 等角度,逐步說明如何在 TypeScript 中正確、有效地定義方法。
適合剛開始接觸 TypeScript 的新手,也能為已有 JavaScript 基礎的開發者提供進階的型別技巧。


核心概念

1. 基本語法:在介面裡宣告方法

介面(interface)可以直接寫出函式簽名,語法與普通函式宣告相同,只是放在介面內。

interface Person {
  /** 姓名 */
  name: string;
  /** 年齡 */
  age: number;
  /** 方法:自我介紹 */
  introduce(): string;
}

使用時,只要物件符合介面的結構即可:

const alice: Person = {
  name: "Alice",
  age: 28,
  introduce() {
    return `Hi, I'm ${this.name}, ${this.age} 歲。`;
  }
};

console.log(alice.introduce()); // Hi, I'm Alice, 28 歲。

重點:方法的 this 會自動被推斷為介面本身(Person),因此在方法內可以安全存取 nameage


2. 可選與只讀函式屬性

有時候方法不是必須的,或是希望方法一旦實作就不能被覆寫,這時可使用 可選屬性 (?)只讀屬性 (readonly)

interface Logger {
  /** 必須的 log 方法 */
  log(message: string): void;
  /** 可選的 error 方法 */
  error?(error: Error): void;
  /** 只讀的 flush 方法,實作後不可改寫 */
  readonly flush: () => void;
}
const consoleLogger: Logger = {
  log(msg) { console.log(msg); },
  flush() { console.log("flush"); }
  // error 方法可以省略
};

// 下面會編譯錯誤:flush 為 readonly,不能重新指派
// consoleLogger.flush = () => console.log("new");

3. this 型別與箭頭函式

在介面方法裡,this 會被推斷為介面本身。但若使用 箭頭函式=>),this 會被「捕獲」自外層,可能不符合預期。

interface Counter {
  /** 正常方法:this 被推斷為 Counter */
  inc(): void;
  /** 使用箭頭函式:this 會是外層的 this(例如全域) */
  dec: () => void;
}
const counter: Counter = {
  value: 0,
  inc() {
    // this 正確指向 counter
    this.value++;
  },
  // 使用箭頭函式會導致 this 為 undefined(strict mode)
  dec: () => {
    // this.value 會產生錯誤
    // this.value--;
  }
};

最佳實踐:除非有特別需求,介面的方法一般採用 普通函式宣告,讓 this 能正確推斷。

如果真的需要固定 this,可以在函式簽名中明確寫出 this 型別:

interface Counter {
  value: number;
  inc(this: Counter): void;   // 明確指定 this 為 Counter
}

4. 函式重載於介面

有時同一個方法會接受不同的參數組合,TypeScript 允許在介面中寫 重載簽名,最後只保留一個實作。

interface Formatter {
  /** 重載簽名 1:接受字串 */
  format(value: string): string;
  /** 重載簽名 2:接受數字 */
  format(value: number, locale?: string): string;
  /** 真正的實作(只能寫一次) */
  format(value: any, locale?: string): string;
}
const fmt: Formatter = {
  format(value: any, locale?: string): string {
    if (typeof value === "number") {
      return value.toLocaleString(locale);
    }
    return value.trim();
  }
};

console.log(fmt.format("  hello  ")); // "hello"
console.log(fmt.format(1234567, "en-US")); // "1,234,567"

技巧:介面的重載順序由寬到窄比較好,編譯器會依序匹配最符合的簽名。


5. 泛型函式屬性

若方法的參數或回傳值與介面的型別參數有關,使用 泛型 可以讓介面更具彈性。

interface Repository<T> {
  /** 取得單一項目 */
  get(id: string): T | undefined;
  /** 新增或更新 */
  save(item: T): void;
  /** 取得多筆資料,支援過濾條件 */
  find<K extends keyof T>(criteria: Partial<Pick<T, K>>): T[];
}
type User = { id: string; name: string; age: number };

const userRepo: Repository<User> = {
  get(id) { /* ... */ return undefined; },
  save(item) { /* ... */ },
  find(criteria) {
    // 這裡 criteria 只允許 id、name、age 任意組合
    return [];
  }
};

優點:呼叫 userRepo.find({ age: 30 }) 時,編譯器會提示只能使用 User 的屬性,避免寫錯欄位名稱。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
把方法寫成屬性 (prop: () => void),卻忘記 this 會被捕獲 this 變成 undefined,執行時拋出錯誤 盡量使用 普通方法宣告 (method(): void);若一定要用屬性,手動指定 this 型別或使用 bind
忘記在介面中加入 this 型別,導致 this 被推斷為 any 失去型別安全,編譯器不會警告錯誤 在需要時顯式寫 this: InterfaceName,或使用 noImplicitThis 編譯選項。
重載簽名與實作不一致 編譯錯誤 Implementation signature is not compatible 確認最後的實作簽名能覆蓋所有重載,且參數型別使用最寬鬆的 any 或聯合型別。
把可選方法寫成必選,但實作時忘記提供 物件不符合介面,編譯失敗 依需求使用 ?,或在介面中提供預設空函式 (method?: () => void = () => {})。
在介面裡使用 readonly 函式屬性,卻在實作時重新指派 編譯錯誤 Cannot assign to 'method' because it is a read-only property 只在需要防止覆寫時使用 readonly,否則省略。

最佳實踐

  1. 保持方法宣告的統一性:除非有特殊需求,建議全部使用 method(): ReturnType 形式,讓 this 自動推斷。
  2. 啟用嚴格模式strict, noImplicitThis)以捕捉隱藏的 this 錯誤。
  3. 為公共介面加上 JSDoc,提供說明與範例,提升團隊成員的可讀性。
  4. 在介面中盡量使用泛型,讓方法能適應不同資料型別,減少重複程式碼。
  5. 測試 this 行為:使用 console.log(this) 或單元測試驗證方法內的 this 是否如預期。

實際應用場景

1. UI 元件的事件處理

在大型前端專案中,常會把元件的事件處理抽象成介面:

interface ButtonProps {
  label: string;
  /** 點擊時的回呼 */
  onClick: (event: MouseEvent) => void;
  /** 可選的滑鼠移入事件 */
  onMouseEnter?: (event: MouseEvent) => void;
}

這樣的寫法讓每個按鈕實作只需要提供符合型別的函式,且 TypeScript 能夠在編譯期即提示缺少必要的 onClick

2. 資料存取層(Repository Pattern)

前面提到的 Repository<T> 介面,是企業級應用常見的 資料抽象層。透過泛型函式屬性,我們可以在不同的資料庫(MySQL、MongoDB、IndexedDB)之間切換,而不必改動使用者的程式碼。

3. 狀態機(State Machine)

狀態機的每個狀態往往都有「進入」與「離開」的行為,使用介面定義函式屬性可讓狀態物件自我描述:

interface State {
  /** 進入此狀態時執行 */
  onEnter(this: State, context: any): void;
  /** 離開此狀態時執行 */
  onExit?(this: State, context: any): void;
}

在實作時,this 會自動指向當前狀態物件,保證可以安全存取狀態內部的屬性。


總結

  • 函式屬性是介面最核心的功能之一,讓物件不只是資料的容器,更能封裝行為。
  • 正確的 方法宣告(普通函式 vs. 箭頭函式)能確保 this 的型別安全;可選只讀屬性則提供彈性與防護。
  • 重載泛型使介面能同時支援多種呼叫方式與資料型別,提升程式碼的可重用性。
  • 避免常見陷阱(this 捕獲、重載不匹配等)並遵守最佳實踐,能讓 TypeScript 在大型專案中發揮最大效能。

掌握了這些技巧後,你就能在 TypeScript 中以介面安全、清晰地描述物件的行為,寫出更健壯、更易維護的程式碼。祝你在日常開發與團隊協作中,玩得開心、寫得順手!