本文 AI 產出,尚未審核

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 斷言

最佳實踐

  1. 搭配 ConstructorParameters<T>:在需要傳遞建構子參數時,同時使用兩個工具型別,確保 參數與回傳型別 完全對應。
  2. 限制泛型:在泛型函式中加入 extends new (...args: any) => any,讓編譯器在使用 InstanceType 前就檢查型別正確性。
  3. 保持可讀性:如果 InstanceType<T> 的結果過於抽象,考慮使用 type Alias = InstanceType<typeof MyClass> 為其取一個易懂的別名。
  4. 避免過度抽象:在公共 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 程式碼。祝你寫程式快快樂樂、型別永遠正確!