TypeScript 類別(Classes)- 實作介面(implements)
簡介
在大型前端或 Node.js 專案中,型別安全是維持程式碼可讀、可維護的根本。TypeScript 透過 介面(interface) 讓開發者能以宣告式的方式描述物件的結構,而 類別(class) 則提供了封裝、繼承與多型等物件導向特性。
當我們希望一個類別必須遵守某套規範時,就會使用 implements 關鍵字讓類別 實作介面。透過 implements,編譯器會在編譯階段檢查類別是否完整實作介面所定義的屬性與方法,從而避免執行時的錯誤。
本篇文章將深入說明 implements 的語法與運作機制,提供多個實用範例,並探討常見陷阱與最佳實踐,讓你在日常開發中能自信地運用介面與類別的結合。
核心概念
1. 介面的基本語法
介面本身只是一個型別宣告,不會產生任何 JavaScript 程式碼。最簡單的介面範例如下:
// 定義一個描述使用者資訊的介面
interface User {
/** 使用者名稱 */
name: string;
/** 年齡,必須是正整數 */
age: number;
/** 取得簡介的函式 */
getInfo(): string;
}
interface後接介面名稱(慣例使用 PascalCase)。- 介面內可以包含屬性、方法、索引簽名、甚至是其他介面的合併(declaration merging)。
2. implements 與類別的關係
當類別使用 implements 時,TypeScript 只會 檢查型別相容性,不會改變類別的原型鏈。以下範例展示最基本的實作方式:
class Person implements User {
// 必須提供 name、age 與 getInfo
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getInfo(): string {
return `${this.name} (${this.age} 歲)`;
}
}
重點:即使介面裡的屬性是
readonly,類別仍須以相同的可讀性實作;若介面要求optional(?),類別可以選擇實作或不實作。
3. 多重介面實作
TypeScript 允許一個類別同時實作多個介面,只要所有介面的要求都被滿足即可。這在「組合多個職能」的情境中特別有用:
interface Logger {
log(message: string): void;
}
interface Serializer {
serialize(): string;
}
class Service implements Logger, Serializer {
private data: any;
constructor(data: any) {
this.data = data;
}
log(message: string): void {
console.log(`[Log] ${message}`);
}
serialize(): string {
return JSON.stringify(this.data);
}
}
- 類別
Service同時具備log與serialize兩個方法,符合Logger與Serializer的需求。 - 若任一介面的成員缺失,編譯器會立即報錯,避免遺漏。
4. 介面繼承與類別實作
介面本身可以 繼承 其他介面,形成層級結構。類別只需要 implements 最終的子介面,編譯器會自動檢查所有父介面的成員:
interface Base {
id: number;
}
interface Timestamped extends Base {
createdAt: Date;
updatedAt: Date;
}
class Record implements Timestamped {
id: number;
createdAt: Date;
updatedAt: Date;
constructor(id: number) {
this.id = id;
this.createdAt = new Date();
this.updatedAt = new Date();
}
}
Timestamped繼承Base,因此Record必須同時具備id、createdAt、updatedAt三個屬性。
5. 抽象類別 vs 介面
| 特性 | 抽象類別(abstract class) | 介面(interface) |
|---|---|---|
| 可包含實作 | 可以有已實作的方法與屬性 | 只能宣告型別,無實作 |
| 多重繼承 | 只能 extends 一個抽象類別 |
可以 extends 多個介面 |
| 建構子 | 可以定義建構子參數 | 無建構子(只能描述結構) |
| 執行時行為 | 會產生 JavaScript 代碼 | 完全在編譯階段消失 |
實務建議:若只需要描述「形狀」而不涉及任何預設行為,使用介面;若需要提供部分共用實作或建構子邏輯,則考慮抽象類別。
6. 完整範例:從介面到類別的完整流程
以下示範一個簡易的 事件系統,結合介面、類別與多重實作:
// 1. 定義事件介面
interface Event {
type: string;
payload: any;
}
// 2. 定義事件處理器介面
interface EventHandler {
/** 處理特定類型的事件 */
handle(event: Event): void;
}
// 3. 定義可記錄的介面
interface Loggable {
/** 記錄訊息 */
log(message: string): void;
}
// 4. 實作類別,同時具備 EventHandler 與 Loggable
class ClickEventHandler implements EventHandler, Loggable {
handle(event: Event): void {
if (event.type === 'click') {
this.log(`處理 Click 事件,payload: ${event.payload}`);
// 實際處理邏輯…
}
}
log(message: string): void {
console.log(`[ClickEvent] ${message}`);
}
}
// 5. 使用
const handler = new ClickEventHandler();
handler.handle({ type: 'click', payload: { x: 120, y: 80 } });
ClickEventHandler同時符合EventHandler與Loggable兩個介面的合約。- 若忘記實作
log方法,編譯器會直接提示錯誤,保證類別的完整性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 忘記實作介面成員 | 編譯錯誤往往只在開發階段出現,若使用 any 逃過檢查會在執行時出錯。 |
嚴格開啟 noImplicitAny,讓編譯器必須檢查所有成員。 |
| 介面屬性可選 vs 類別必須實作 | 介面裡的 ? 代表可選,類別若寫成必填會降低彈性。 |
根據需求決定:若實作類別永遠需要該屬性,將介面改為必填;若不一定,保留 ?。 |
| 使用類別型別作為介面 | 直接把類別當作介面使用會產生結構不一致的問題(例如私有屬性)。 | 使用 interface 或 type 來抽離結構,避免把實作類別混入介面。 |
| 介面繼承衝突 | 多個父介面定義同名屬性但型別不一致。 | 統一介面設計,或在子介面中重新定義正確型別。 |
| 抽象類別與介面的混用 | 同時使用抽象類別和介面可能造成重複定義。 | 只選其一:若需要共用實作,選抽象類別;若僅需型別,選介面。 |
最佳實踐
- 介面命名:以
I開頭(如IUser)或直接使用描述性名稱(如User)皆可,保持團隊一致性。 - 單一職責:每個介面只描述一組相關行為,避免「肥介面」(God Interface)。
- 使用
readonly:若屬性在實作後不應被更改,於介面中加上readonly,提升不可變性。 - 利用泛型:介面可接受泛型參數,讓類別在不同情境下重用同一套合約。
interface Repository<T> {
findById(id: string): T | undefined;
save(entity: T): void;
}
- 測試:透過 型別測試(type tests)或 interface mock,確保類別實作符合預期。
實際應用場景
REST API 客戶端
- 介面定義每個 API 回傳的資料型別(
UserResponse、PostResponse),類別ApiService實作fetch、post方法,確保所有呼叫都符合介面規範。
- 介面定義每個 API 回傳的資料型別(
插件化系統
- 主程式定義
Plugin介面(initialize、destroy),第三方插件以類別實作該介面,主程式在載入時僅依賴介面,不必關心具體實作。
- 主程式定義
領域驅動設計(DDD)
Repository、Specification、DomainService等概念皆以介面描述,具體的資料庫存取類別(如MongoRepository)則implements這些介面,保持領域層與基礎設施層的解耦。
測試替身(Test Double)
- 介面提供測試雙(mock)物件的型別基礎,測試時只需要提供符合介面的假實作,而不必載入真實的類別。
總結
implements讓 類別必須遵守介面 的合約,提供編譯期的型別安全。- 介面本身只描述結構,可透過 繼承、泛型與多重實作 組合出彈性高的型別系統。
- 了解 抽象類別 vs 介面 的差異,根據需求選擇最適合的抽象方式。
- 常見的陷阱包括忘記實作成員、介面屬性可選與類別必填不一致等,透過嚴格的編譯設定與良好的命名規範可有效避免。
掌握了介面的實作方式後,你就能在 TypeScript 專案中建立 清晰、可維護且具備高度型別保證 的程式碼基礎,無論是開發 API 客戶端、插件系統,或是大型企業級應用,都能從中受益。祝你在 TypeScript 的旅程中寫出更安全、更優雅的程式!