TypeScriptでの型定義、日々お疲れ様です。
中級へのステップを駆け上がっている君なら、`Partial
だがな、現場のコードベースをさらに深く、そして美しく抽象化しようとした時、避けて通れないのが「コンストラクタ(クラス)」と「インスタンス」の型上の乖離だ。
「クラスをそのまま型として使えばいいじゃないか」――そう思ったかい?
実は、クラス名(例:`class User {}`)を型注釈に使うとき、TypeScriptはそれを「インスタンスの型」として扱っている。ここまでは基本だ。
しかし、そのクラス自体のコンストラクタ関数を引数として受け取ったり、ファクトリー関数を作ったり、あるいはサードパーティのインスタンス生成ロジックと型安全に結合しようとした瞬間、途端に足元をすくわれる。
そこで登場するのが、今回解説する `InstanceType
このユーティリティ型をモノにするだけで、君の書くTypeScriptの型安全性は一段と「プロの仕事」に仕上がる。さあ、深掘りしていこうか。
—
1. なぜ `InstanceType` が必要なのか?(TypeScriptの標準仕様とブラウザの裏側)
まず大前提として、TypeScriptのクラスはコンパイル後、JavaScriptのプロトタイプベースのコンストラクタ関数(ES6以降は`class`構文)に変換される。
ブラウザのJavaScriptエンジン(V8など)が実際にメモリ上で処理するのは、`new`キーワードによってインスタンスが生成され、`__proto__`チェーンが構築されるという手続きだ。
ここでTypeScriptの型システムに目を向けてみよう。
クラス定義が存在するとき、実は二つの異なる型が存在している。
1. クラスのインスタンスの型(例:`User`)
2. クラスのコンストラクタ(クラス自体)の型(例: `typeof User`)
この「クラスそのものの型(コンストラクタ関数型)」から、「そこから生まれるインスタンスの型」を逆引きしたい、抽出したいという要件が、実務の設計では頻繁に発生する。
それを自前で書こうとすると、こんな面倒な型を書くことになる。
// 自前でコンストラクタ型からインスタンス型を抽出する例(車輪の再発明)
type MyInstanceType
……おいおい、こんな複雑なConditional Typesと`infer`のボイラープレートを毎回書きたくないよな?
だからこそ、TypeScript公式が最初から用意してくれているのが `InstanceType
—
2. 現場ですぐに使える!実践的コード例
百聞は一見にしかずだ。実際のフロントエンド開発で遭遇しそうな、少し歯ごたえのあるユースケースを見てみよう。
以下のコードを君のエディタに貼り付けてみてくれ。コンストラクタ型をDI(依存性注入)やファクトリーパターンで扱う際の、極めて実践的な例だ。
// ==========================================
// 実践:APIクライアントのプラグイン機構を作る
// ==========================================
// 1. ベースとなる抽象クラス(すべてのAPIモジュールの親)
abstract class BaseApiModule {
constructor(protected baseUrl: string) {}
abstract healthCheck(): Promise
}
// 2. 具体的なAPIモジュール群
class UserApiModule extends BaseApiModule {
async healthCheck(): Promise
// 実際の通信処理(ここでは簡易的にtrueを返す)
console.log(`Checking health for ${this.baseUrl}/users`);
return true;
}
// ユーザー固有のメソッド
async getUser(id: string) {
return { id, name: ‘Taro Yamada’ };
}
}
class ProductApiModule extends BaseApiModule {
async healthCheck(): Promise
console.log(`Checking health for ${this.baseUrl}/products`);
return true;
}
// プロダクト固有のメソッド
async getProduct(sku: string) {
return { sku, title: ‘TypeScript Deep Dive’ };
}
}
// 3. コンストラクタ関数の型定義(抽象クラスのコンストラクタを受け取る型)
// new (…args: any[]) => T は「newしてインスタンス化できる関数(コンストラクタ)」を表す
type ApiModuleConstructor
// 4. ファクトリー関数の実装
// ここで InstanceType
// 引数に渡されたコンストラクタ型 T から、確実にそのインスタンス型を復元して戻り値の型にする。
function createApiModule
ModuleClass: T,
baseUrl: string
): InstanceType
// 実際にインスタンスを生成して返す
return new ModuleClass(baseUrl) as InstanceType
}
// ==========================================
// 動作確認(型推論の恩恵を味わう)
// ==========================================
// UserApiModuleのコンストラクタを渡す
const userApi = createApiModule(UserApiModule, ‘https://api.example.com’);
// IDEの補完が完璧に効く! userApi は UserApiModule 型として推論されている
// 試しに getUser を呼び出してみよう
userApi.getUser(‘123’).then(user => {
console.log(user.name); // ‘Taro Yamada’
});
// ProductApiModuleのコンストラクタを渡す
const productApi = createApiModule(ProductApiModule, ‘https://api.example.com’);
// こちらも当然、ProductApiModule として完璧に推論される
productApi.getProduct(‘TS-101’).then(product => {
console.log(product.title); // ‘TypeScript Deep Dive’
});
このコードのどこが優れているか分かるかい?
`createApiModule` という汎用的なファクトリー関数を作っているにもかかわらず、渡すクラス(`UserApiModule` なのか `ProductApiModule` なのか)に応じて、戻り値の型が完全に動的に決定されている点だ。
もし `InstanceType
—
3. シニアから後輩へのアドバイス:実務での注意点とアンチパターン
最後に、現場で `InstanceType
① `typeof` との組み合わせを間違えるな
初学者がやりがちなミスとして、インスタンスそのものを `InstanceType` に渡してしまうケースがある。
const userInstance = new UserApiModule(‘https://…’);
// ❌ 間違い:インスタンスそのものに typeof は使えない(いや、使えるが期待した型にならない)
type Bad = InstanceType
// ⭕️ 正解:コンストラクタ(クラスそのもの)の型に対して使う
type Good = InstanceType
クラス名(`UserApiModule`)をそのまま型として書いた場合は「インスタンス型」になるが、変数名やクラスオブジェクト自体から型を取り出したいときは `typeof UserApiModule`(クラスのコンストラクタ型)に包む必要がある。この違いを指の感覚で覚えることだ。
② クラスではなく「通常の関数」には使えない
`InstanceType
つまり、`new` キーワードでインスタンス化できない通常の関数(プレーンなアロー関数やファクトリー関数)を渡すと、容赦なく型エラーになる。
「オブジェクトの設計図としてのクラス」や「コンストラクタ関数」を扱うための専用ツールであることを忘れないでほしい。
—
まとめ
`InstanceType
フレームワークが用意してくれた魔法をただ使うだけでなく、その裏側にある「型と実体の関係」を理解してこそ、ワンランク上のフロントエンド・アーキテクトと言える。
明日からのコードに、ぜひこの知見を組み込んでみてくれ。君の書くコードの洗練度を楽しみにしているよ。

コメント