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),因此在方法內可以安全存取name、age。
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,否則省略。 |
最佳實踐:
- 保持方法宣告的統一性:除非有特殊需求,建議全部使用
method(): ReturnType形式,讓this自動推斷。 - 啟用嚴格模式(
strict,noImplicitThis)以捕捉隱藏的this錯誤。 - 為公共介面加上 JSDoc,提供說明與範例,提升團隊成員的可讀性。
- 在介面中盡量使用泛型,讓方法能適應不同資料型別,減少重複程式碼。
- 測試
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 中以介面安全、清晰地描述物件的行為,寫出更健壯、更易維護的程式碼。祝你在日常開發與團隊協作中,玩得開心、寫得順手!