フロントエンドの現場で「おっ、やるな」とコードレビューで唸らせる瞬間はどんな時か知ってるかい?
それは、無駄な型定義を書き殴るのをやめ、TypeScriptが標準で持っているユーティリティ型をエレガントに使いこなしているを見た時だ。特に、クラスベースの設計や、外部ライブラリのインスタンスを扱うときに `InstanceType
今日は、中級から一歩抜け出して「真のシニア」に近づくための武器、`InstanceType
—
なぜ `InstanceType` が必要なのか?
日々のフロントエンド開発で、クラス(Class)を定義することは減ったかもしれない。関数型やオブジェクト指向のハイブリッド、あるいはReactのコンポーネント指向が主流だからね。しかし、複雑な状態管理クラス、Web APIのラッパー、独自のエラーハンドリング、あるいは Three.js などのサードパーティ製グラフィクスライブラリを扱うとき、依然として「クラス」や「コンストラクタ関数」に直面する場面はある。
ここで一つ質問だ。あるクラスが生成する「インスタンスの型」が欲しい時、君はどう書いているだろうか?
class ApiClient {
constructor(public endpoint: string) {}
request() { / … / }
}
// やりがちな、インスタンスの型を手動で再定義しちゃうパターン(絶対にやめよう!)
// type ApiClientInstance = { endpoint: string; request: () => void; };
ちょっと待ってくれ。クラスの定義が変わるたびに、手動で型を書き換えるのかい? そんなのはDRY原則(Don’t Repeat Yourself)に反するし、何よりコードの変更に怯えながら生きる羽目になる。
TypeScriptの世界では、「クラスそのもの(コンストラクタ関数)」と「そのクラスから生み出されるインスタンス」は、まったく別の型として扱われる。この「クラスからインスタンスの型を自動で引っペがす」ために存在するのが、今回主役の `InstanceType
—
`InstanceType` の正体と、JavaScriptの裏側の仕組み
`InstanceType
JavaScriptにおいて、クラスは実態として「コンストラクタ関数(Functionオブジェクト)」だ。そして、`new` 演算子をつけて呼び出すことで、プロトタイプチェーンを継承したオブジェクト(インスタンス)がメモリ上に生成される。
TypeScriptの型システムも、このランタイムの構造を忠実にシミュレートしている。
TypeScriptの内部実装(lib.es5.d.tsなど)を覗いてみると、`InstanceType
type InstanceType
T extends abstract new (…args: any) => infer R ? R : never;
おっと、見慣れない記号が並んでウッとなったかい? 怖がる必要はない。分解して読んでみよう。
1. `T extends abstract new (…args: any) => any`
「もし、型 `T` が `new` で呼び出せるコンストラクタ関数(抽象クラス含む)であるならば」という制約。
2. `infer R`
「そのコンストラクタ関数が `new` したときに返す(生成する)インスタンスの型を推論して、それを `R` という仮の名前でキャプチャしろ」という意味。
3. `? R : never`
「推論に成功したらその型 `R` を返し、失敗したら `never` にする」という三項演算子。
つまり、`InstanceType
—
現場で使える!実践コード例
百聞は一見に如かず。実務でよくあるユースケースを想定したコードを見ていこう。
以下のコードは、そのままコピペしてTypeScriptのプレイグラウンドや手元のエディタで動かせるように書いてある。
/
- 複雑なダイアログを管理するUIクラス
/
class ModalController {
private isOpen: boolean = false;
constructor(private modalId: string) {}
public open(data?: Record
this.isOpen = true;
console.log(`Modal [${this.modalId}] opened with`, data);
}
public close(): void {
this.isOpen = false;
console.log(`Modal [${this.modalId}] closed`);
}
}
// —————————————————————–
// ケース1: クラスのインスタンス型を安全に抽出する
// —————————————————————–
// クラス自体(コンストラクタ関数)の型は typeof ModalController
type ModalControllerClass = typeof ModalController;
// InstanceTypeを使ってインスタンスの型を取得する
type ModalInstance = InstanceType
// 検証:正しくインスタンスの型が取れているか
const handleModalReady = (modal: ModalInstance) => {
modal.open({ userId: 123 }); // OK
// modal.isOpen; // Error: Property ‘isOpen’ is private on type ‘ModalController’
};
// —————————————————————–
// ケース2: ファクトリー関数やDI(依存性注入)コンテナでの活用
// —————————————————————–
/
- 任意のクラスを受け取り、それをインスタンス化して返す汎用ファクトリー関数
- ここで InstanceType
が爆発的な真価を発揮する
/
function createInstance
TargetClass: T,
…args: ConstructorParameters
): InstanceType
// 裏側で実際に new してインスタンスを生成
return new TargetClass(…args);
}
// 利用例:型安全なインスタンス生成
// 第1引数にクラス、第2引数以降にコンストラクタの引数を渡す
const myModal = createInstance(ModalController, “user-profile-modal”);
// myModal の型は自動的に ModalController に推論されるため、補完が完璧に効く
myModal.open({ theme: “dark” });
この `createInstance` ファクトリーの例を見てほしい。
`ConstructorParameters
—
シニアからの実践アドバイス・注意点
最後に、実務でこのテクニックを使うときの「生きた知見」をいくつか共有しておこう。
1. `typeof ClassName` との混同に注意する
初心者がやりがちなミスとして、クラスを型として指定するときに `let a: ModalController` と書くのは、実はTypeScriptでは「インスタンスの型」を指すので偶然うまくいく。しかし、クラスそのもの(コンストラクタ)を型として扱いたい場合は `typeof ModalController` と書く必要がある。この違い(クラスそのものの型 vs インスタンスの型)をチームメンバーに説明できるようにしておこう。
2. サードパーティライブラリの型拡張で困ったとき
外部ライブラリが「インスタンスの型は公開しているが、コンストラクタの型やクラス自体がエクスポートされていない」というアンチパターンに遭遇することが稀にある。逆に、クラスはあるのにインスタンス型が欲しい場合など、`InstanceType
型定義は単なるエラーチェックの道具ではなく、「コードの意図を正確にコンパイラとチームメイトに伝えるドキュメント」だ。
`InstanceType

コメント