TypeScript 教學:工具型別 InstanceType<T>
簡介
在日常的 TypeScript 開發中,我們常會遇到需要從 建構子(constructor) 取得實例型別的情境。傳統上,我們只能靠手動寫出介面或型別別名,既繁瑣又容易遺漏。InstanceType<T> 正是為了解決這個問題而設計的 工具型別(Utility Types),它可以自動推斷傳入建構子 T 所產生的實例型別。
使用 InstanceType<T> 能讓我們:
- 減少重複代碼:不必再手動為每個 class 撰寫對應的型別。
- 提升型別安全:在函式、泛型或工廠模式中,能確保返回值的型別正確對應到實際的 class。
- 增進可讀性:程式碼表達意圖更清晰,維護成本下降。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直到真實的應用情境,帶你一步步掌握 InstanceType<T> 的使用方式。
核心概念
1. InstanceType<T> 的基本定義
InstanceType<T> 是 TypeScript 內建的條件型別(Conditional Types),其宣告大致如下(簡化版):
type InstanceType<T extends new (...args: any) => any> =
T extends new (...args: any) => infer R ? R : any;
T extends new (...args: any) => any:限制T必須是「可被new呼叫」的建構子型別。infer R:利用infer推斷建構子回傳的實例型別R。- 結果:如果
T符合條件,就回傳推斷出的R,否則回傳any。
重點:
InstanceType<T>只能接受「建構子型別」作為參數,對於普通函式或物件型別會直接得到any(或編譯錯誤)。
2. 基本範例:從 class 取得實例型別
class User {
constructor(public name: string, public age: number) {}
greet() {
console.log(`Hello, ${this.name}`);
}
}
// 直接使用 InstanceType 取得 User 的實例型別
type UserInstance = InstanceType<typeof User>;
const u: UserInstance = new User('Alice', 30);
u.greet(); // Hello, Alice
typeof User取得 建構子型別,InstanceType再把它轉換成 實例型別UserInstance。- 之後
u的型別即為User的實例,可直接使用所有成員。
3. 搭配泛型:工廠函式
在需要根據傳入的 class 動態產生實例時,InstanceType 能讓函式的回傳型別正確推斷。
function createInstance<T extends new (...args: any) => any>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...args);
}
// 使用
class Point {
constructor(public x: number, public y: number) {}
}
const p = createInstance(Point, 10, 20);
// p 的型別被推斷為 Point
ConstructorParameters<T>取得建構子的參數型別陣列,確保傳入的參數正確。InstanceType<T>讓回傳值型別自動等於Point。
4. 與 ReturnType<T> 的差異
| 工具型別 | 目的 | 接受的型別 |
|---|---|---|
ReturnType<T> |
取得 函式 的回傳型別 | (…args: any) => any |
InstanceType<T> |
取得 建構子 所產生的 實例型別 | new (...args: any) => any |
function makeUser() {
return new User('Bob', 25);
}
type R = ReturnType<typeof makeUser>; // User
type I = InstanceType<typeof User>; // User
兩者在概念上相似,但適用的情境不同,切勿混用。
5. 進階範例:條件型別結合 InstanceType
假設我們想要根據不同的 class 判斷是否具有某個屬性,並在型別層級做分支:
type HasName<T> = T extends { name: string } ? true : false;
type Test<T extends new (...args: any) => any> = HasName<InstanceType<T>>;
class Person {
constructor(public name: string) {}
}
class Counter {
constructor(public count: number) {}
}
type PersonHasName = Test<typeof Person>; // true
type CounterHasName = Test<typeof Counter>; // false
- 先用
InstanceType<T>取得實例型別,再以條件型別檢查是否擁有name屬性。 - 這樣的寫法在 型別守衛、自動化文件生成 等情境非常有用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 傳入非建構子型別 | InstanceType<number> 會得到 any(或編譯錯誤) |
確保使用 typeof ClassName,或在泛型上加上 extends new (...args: any) => any 限制 |
| 抽象類別 | 抽象類別本身無法直接 new,但仍可取得實例型別(抽象子類別的實例) |
使用 abstract new (...args: any) => any 作為限制,或在實作時使用具體子類別 |
| 多重繼承(mixin) | 產生的型別可能混合多個建構子,InstanceType 只能接受單一建構子 |
先把 mixin 組合成一個 class,再對該 class 使用 InstanceType |
| 泛型建構子 | class Box<T> { constructor(public value: T) {} },直接 InstanceType<typeof Box> 會得到 Box<any> |
使用 InstanceType<Box<string>> 需要先具體化泛型,或在工廠函式中使用 new <T>() 的方式 |
| 遞迴型別 | 若建構子回傳自身的子類別,InstanceType 仍能正確推斷,但 IDE 可能顯示 any |
在複雜遞迴情況下,加入明確的型別註解或使用 as const 斷言 |
最佳實踐
- 搭配
ConstructorParameters<T>:在需要傳遞建構子參數時,同時使用兩個工具型別,確保 參數與回傳型別 完全對應。 - 限制泛型:在泛型函式中加入
extends new (...args: any) => any,讓編譯器在使用InstanceType前就檢查型別正確性。 - 保持可讀性:如果
InstanceType<T>的結果過於抽象,考慮使用type Alias = InstanceType<typeof MyClass>為其取一個易懂的別名。 - 避免過度抽象:在公共 API 中盡量提供具體的型別,而非直接暴露
InstanceType,以免使用者因型別推斷失敗而產生困惑。
實際應用場景
1. 依賴注入(DI)容器
DI 容器通常會根據註冊的 class 自動建立實例。使用 InstanceType 可以讓容器的 resolve 方法回傳正確的型別:
class Container {
private map = new Map<Function, any>();
register<T extends new (...args: any) => any>(Ctor: T, instance?: InstanceType<T>) {
this.map.set(Ctor, instance ?? new Ctor());
}
resolve<T extends new (...args: any) => any>(Ctor: T): InstanceType<T> {
const found = this.map.get(Ctor);
if (!found) throw new Error('未註冊的類別');
return found as InstanceType<T>;
}
}
// 使用
class Logger {
log(msg: string) { console.log(msg); }
}
const container = new Container();
container.register(Logger);
const logger = container.resolve(Logger); // logger 型別為 Logger
logger.log('DI works!');
2. 型別安全的工廠模式
在大型程式碼庫中,常會根據字串鍵或配置動態產生不同類別的實例。InstanceType 能讓工廠的回傳型別保持精確:
type ServiceMap = {
user: typeof UserService;
product: typeof ProductService;
};
class UserService { /* ... */ }
class ProductService { /* ... */ }
function serviceFactory<K extends keyof ServiceMap>(key: K): InstanceType<ServiceMap[K]> {
const map: ServiceMap = {
user: UserService,
product: ProductService,
};
return new map[key]();
}
// 呼叫
const userSrv = serviceFactory('user'); // 型別為 UserService
const prodSrv = serviceFactory('product'); // 型別為 ProductService
3. 自動化測試生成器
測試框架常需要根據測試案例自動產生被測試類別的實例。利用 InstanceType 可以保證測試碼的型別安全:
function mockInstance<T extends new (...args: any) => any>(Ctor: T): jest.Mocked<InstanceType<T>> {
const mock = jest.fn(() => ({})) as any;
return new Proxy(new Ctor(), {
get(_, prop) {
return mock[prop] ?? jest.fn();
},
});
}
// 範例
class ApiService {
fetch() { /* 真正的實作 */ }
}
const mockedApi = mockInstance(ApiService);
mockedApi.fetch.mockResolvedValue({ data: 'test' });
總結
InstanceType<T> 是 TypeScript 提供的 實用工具型別,讓開發者能在 編譯階段 即取得建構子所產生的實例型別。透過它,我們可以:
- 減少手動撰寫型別,提升開發效率。
- 確保函式、工廠、DI 容器等返回值的型別安全。
- 結合其他條件型別(如
ConstructorParameters<T>、ReturnType<T>)打造更強大的泛型工具。
在實務開發中,配合 嚴格的泛型限制、具名別名 以及 清晰的型別註解,InstanceType<T> 能夠成為提升程式碼可維護性與可讀性的關鍵利器。希望本篇教學能幫助你在日常專案裡,輕鬆運用 InstanceType 產出更安全、更易懂的 TypeScript 程式碼。祝你寫程式快快樂樂、型別永遠正確!