TypeScript – 類別(Classes)
主題:constructor 型別宣告
簡介
在 TypeScript 中,類別(class) 不只是 ES6 的語法糖,它還提供了強大的型別系統支援。constructor 作為類別最重要的入口點,負責建立物件、注入相依性以及初始化屬性。若能正確為 constructor 宣告型別,不僅能在編譯期即捕捉錯誤,還能提升 IDE 的自動完成與文件提示,讓程式碼的可讀性與可維護性大幅提升。
本篇文章將從 基本語法、參數屬性簡寫、重載(overload)、this 型別 以及 泛型建構子 等多個面向,深入說明 constructor 的型別宣告方式,並提供實務範例、常見陷阱與最佳實踐,幫助讀者在日常開發中正確運用這項功能。
核心概念
1. 基本語法與參數型別
在 TypeScript 中,constructor 的參數必須明確標註型別,否則會被推斷為 any,失去型別安全的好處。
class Person {
name: string;
age: number;
// 1️⃣ 明確宣告參數型別
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
重點:若忘記寫型別,
tsc會警告 "Parameter 'name' implicitly has an 'any' type",建議在tsconfig.json中開啟"noImplicitAny": true。
2. 參數屬性簡寫(Parameter Properties)
TypeScript 允許在 constructor 參數前加上存取修飾子(public、private、protected、readonly),自動產生對應的屬性並完成賦值,省去繁瑣的 this.xxx = xxx 程式碼。
class Car {
// 2️⃣ 透過參數屬性一次完成宣告與賦值
constructor(public brand: string, private year: number) {}
getInfo(): string {
// year 為 private,只能在類別內部存取
return `${this.brand} (${this.year})`;
}
}
此寫法的等價展開如下:
class Car {
public brand: string;
private year: number;
constructor(brand: string, year: number) {
this.brand = brand;
this.year = year;
}
}
技巧:若屬性需要
readonly,直接加上readonly修飾子即可,編譯後會產生private readonly成員。
3. 建構子重載(Constructor Overload)
有時候同一個類別需要接受不同形態的參數組合,這時可以使用 重載 來描述多種簽名,實作只保留一個 constructor。
class Point {
x: number;
y: number;
// 3️⃣ 重載簽名
constructor(x: number, y: number);
constructor(coord: [number, number]);
constructor(xOrCoord: number | [number, number], y?: number) {
if (Array.isArray(xOrCoord)) {
[this.x, this.y] = xOrCoord;
} else {
this.x = xOrCoord;
this.y = y!;
}
}
toString(): string {
return `(${this.x}, ${this.y})`;
}
}
使用時,IDE 會根據重載簽名提供正確的提示:
const p1 = new Point(10, 20); // 兩個數字
const p2 = new Point([5, 15]); // 坐標陣列
注意:重載只能寫在 宣告區(簽名)與 實作區(唯一的
constructor)兩段,且實作區的參數型別必須能覆蓋所有簽名。
4. this 型別的精確化
在類別方法中使用 this 時,TypeScript 會自動推斷其型別為該類別本身。但在 繼承 或 混入(mixin) 情境下,this 可能指向子類別,這時可以使用 this 型別 讓方法返回子類別的正確型別。
class Builder {
protected props: Record<string, any> = {};
// 4️⃣ 以 this 為回傳型別,支援鏈式呼叫
set(key: string, value: any): this {
this.props[key] = value;
return this;
}
build(): Record<string, any> {
return this.props;
}
}
class UserBuilder extends Builder {
setName(name: string): this {
return this.set('name', name);
}
setAge(age: number): this {
return this.set('age', age);
}
}
const user = new UserBuilder()
.setName('Alice')
.setAge(30)
.build(); // user 為 { name: 'Alice', age: 30 }
此寫法讓 setName、setAge 可正確回傳 UserBuilder,而非基底 Builder,避免鏈式呼叫時出現型別錯誤。
5. 泛型建構子(Generic Constructor)
有時候類別本身需要根據外部型別參數動態決定屬性型別,這時可以讓 constructor 變成 泛型。
class Repository<T> {
private items: T[] = [];
// 5️⃣ 泛型建構子:接受任意型別的初始陣列
constructor(initialItems: T[] = []) {
this.items = initialItems;
}
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return [...this.items];
}
}
// 使用範例
interface User {
id: number;
name: string;
}
const userRepo = new Repository<User>([{ id: 1, name: 'Bob' }]);
userRepo.add({ id: 2, name: 'Carol' });
在上述例子中,Repository 的 constructor 會根據 T 的實際型別自動推斷 initialItems 的型別,使得 編譯期即保證 陣列內容的正確性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 / 建議 |
|---|---|---|
| 忘記宣告參數型別 | 會導致 any,失去型別安全。 |
開啟 noImplicitAny,或使用 strict 模式。 |
| 重載實作不符合所有簽名 | 會出現「類別建構子實作與重載不相容」錯誤。 | 確保實作參數型別能接受所有重載的可能值,必要時使用聯合型別或類型守衛。 |
在 constructor 中直接使用 this 之前未初始化屬性 |
this 讀取未賦值屬性會報錯。 |
先在屬性宣告時提供預設值,或在 constructor 前寫 !(非空斷言)※慎用。 |
過度使用 any 或 unknown |
失去型別檢查的好處。 | 儘量使用具體型別或泛型,只有在確實無法預測時才使用 unknown,再透過型別守衛縮窄。 |
混用 public、private 參數屬性與手動賦值 |
可能產生重複宣告或未預期行為。 | 選擇其一:若使用參數屬性,則不需要在建構子內再寫 this.xxx = xxx;。 |
最佳實踐
- 始終為
constructor參數加上型別,即使是簡單的string、number。 - 善用參數屬性 簡化程式碼,同時保留存取修飾子以控制可見性。
- 在需要多樣化建構方式時,使用 重載 讓呼叫端得到正確的型別提示。
- 若類別支援鏈式呼叫,使用
this型別返回自身,提升可讀性與安全性。 - 對於資料容器或服務層,考慮使用 泛型建構子,讓類別在不同情境下保持彈性。
實際應用場景
API 客戶端
class ApiClient { private baseUrl: string; private token?: string; constructor(baseUrl: string, token?: string) { this.baseUrl = baseUrl; this.token = token; } // 依據 token 是否提供,動態調整 Header }透過嚴格的
constructor型別,開發者在建立ApiClient時必須提供正確的 URL,若忘記傳入token,編譯器會提示缺少參數。表單驗證器(Validator)
使用 參數屬性 搭配 泛型,一次完成屬性宣告與注入驗證規則:class Validator<T> { constructor(public schema: Partial<Record<keyof T, (value: any) => boolean>>) {} }資料庫模型(ORM)
透過 建構子重載 支援「以 ID 讀取」或「以完整資料建立」兩種方式:class User { id: number; name: string; constructor(id: number); constructor(data: { id: number; name: string }); constructor(arg: number | { id: number; name: string }) { if (typeof arg === 'number') { // 從 DB 讀取 } else { this.id = arg.id; this.name = arg.name; } } }插件系統(Plugin System)
利用this型別 讓插件基底類別返回子類別,使得插件開發者可安全使用鏈式 API:abstract class PluginBase { protected config: Record<string, any> = {}; setOption(key: string, value: any): this { this.config[key] = value; return this; } }
總結
constructor是類別最關鍵的入口,正確的型別宣告 能在編譯期捕捉錯誤、提升 IDE 體驗。- 透過 參數屬性、重載、
this型別 以及 泛型建構子,我們可以寫出 簡潔、彈性且安全 的類別程式碼。 - 常見的陷阱多與 型別遺漏、重載不相容、未初始化屬性 有關,遵守 嚴格模式、使用 型別守衛、避免過度使用
any,即可減少這些問題。
掌握了 constructor 的型別宣告技巧後,你將能在大型專案中更自信地設計 API、封裝商業邏輯,並在團隊協作時提供更清晰的契約(contract)。快把本文的範例搬進你的程式碼庫,讓 TypeScript 為你的類別保駕護航吧!