1. ホーム
  2. TypeScript

【TypeScript】InstanceType・ConstructorParameters の使い方|クラスから型を取り出す

Share

TypeScript でクラスを扱っていると、「このクラスのインスタンスの型を、別の場所でも使い回したい」「ファクトリ関数の引数を、クラスのコンストラクタと同じ型にそろえたい」という場面が出てきます。そんなときに役立つのが、組み込みユーティリティ型の InstanceType<T>ConstructorParameters<T> です。前者はクラスからインスタンスの型を、後者はコンストラクタの引数の型を、それぞれ自動で取り出してくれます。この記事では、両者の基本的な使い方から、なぜ typeof を付けてクラスを渡すのか、そして実践的な使いどころや abstract クラスでの挙動といった注意点までを、具体的なコードとあわせて解説します。

クラスから型を取り出したい場面

クラスを定義すると、そのクラスは「インスタンスの形」と「インスタンスの作り方(コンストラクタ)」という2種類の情報を持ちます。多くの場面ではクラスを直接使えば済みますが、型だけを部分的に取り出したいこともあります。たとえば、あるクラスのインスタンスを受け取る関数を書くときや、コンストラクタと同じ引数を受け取るヘルパー関数を用意するときです。

こうした「クラスの一部を型として抜き出す」作業を手作業でやると、元のクラスを変更したときに型定義の修正漏れが起きます。InstanceType<T>ConstructorParameters<T> を使えば、クラスの定義から型を自動的に導き出せるので、元のクラスに追従して常に正しい型を保てます。どちらも TypeScript に最初から用意されている組み込みのユーティリティ型なので、追加のインストールは不要です。

基本の使い方

まずは題材となるシンプルなクラスを用意します。名前と年齢を受け取る User クラスです。

user.ts
class User {
  constructor(
    public name: string,
    public age: number,
  ) {}
}

この User に対して、2つのユーティリティ型を使ってみます。InstanceType はインスタンスの型(プロパティ nameage を持つオブジェクト)を、ConstructorParameters はコンストラクタの引数の型をタプルとして取り出します。

user.ts
// インスタンスの型を取り出す
type UserInstance = InstanceType<typeof User>;
// → { name: string; age: number }

// コンストラクタ引数の型をタプルで取り出す
type UserArgs = ConstructorParameters<typeof User>;
// → [name: string, age: number]

const taro: UserInstance = new User("太郎", 20);
const args: UserArgs = ["花子", 25];

InstanceType<typeof User>new User(...) で生成されるオブジェクトの型そのものです。ConstructorParameters<typeof User>[string, number] というタプル型になり、コンストラクタに渡す引数の並びと型がそのまま表現されます。引数名(nameage)はラベル付きタプルとして保持されますが、型としての実体は [string, number] です。

なぜ typeof User を渡すのか

ここが最初につまずきやすいポイントです。上のコードで、なぜ InstanceType<User> ではなく InstanceType<typeof User> と書くのか、を整理しておきましょう。理由は、TypeScript ではクラス名が「型」と「値」の2つの顔を持っているからです。

クラス名を型の位置(型注釈など)で使うと、それは「インスタンスの型」を意味します。一方、クラス名を値の位置(変数に代入したり new したりする場所)で使うと、それは「コンストラクタ関数そのもの(クラス本体)」を指します。次のコードで両者の違いを確認してみます。

user.ts
// 型の位置の User = インスタンスの型
const user: User = new User("太郎", 20);

// 値の位置の User = コンストラクタ(クラス本体)
const ctor = User; // ctor の型は「new (name, age) => User」

InstanceType<T>ConstructorParameters<T> が受け取りたいのは、コンストラクタの型、つまり「new で呼べる関数の型」です。ところが型の位置にそのまま User と書くと、それはインスタンスの型になってしまい、コンストラクタの型ではありません。そこで typeof 演算子を使い、値としての User(=コンストラクタ)の型を取り出して渡します。これが typeof User の意味です。

この typeof は JavaScript の実行時に型を調べる typeof とは別物で、TypeScript の型レベルの演算子です。「値の位置にあるものから、その型を取り出す」働きをします。typeof User は「new (name: string, age: number) => User という形のコンストラクタ型」になり、これをユーティリティ型に渡すことで初めて目的の型が得られます。

ファクトリ関数の引数をクラスにそろえる

実践的な使いどころのひとつが、クラスのインスタンスを生成するファクトリ関数です。コンストラクタと同じ引数を受け取ってインスタンスを返す関数を書くとき、引数の型を手で (name: string, age: number) と書くと、クラス側の変更に追従できません。ConstructorParameters と可変長引数(レスト引数)を組み合わせると、コンストラクタの引数にぴったり合わせられます。

factory.ts
// User のコンストラクタ引数をそのまま受け取るファクトリ関数
function createUser(
  ...args: ConstructorParameters<typeof User>
): InstanceType<typeof User> {
  return new User(...args);
}

// 呼び出し側は User のコンストラクタと同じ引数で使える
const user = createUser("太郎", 20);
// createUser(123) // エラー: 型が合わない

引数を ...args: ConstructorParameters<typeof User> とすることで、createUser の引数はコンストラクタと完全に同じ型・同じ並びになります。将来 User のコンストラクタに引数を追加しても、createUser の型は自動で更新されるため、修正漏れが起きません。戻り値も InstanceType<typeof User> で表現しておけば、生成されるインスタンスの型が明示され、読み手にも意図が伝わりやすくなります。

インスタンスの型を別の場所で使い回す

クラスを直接インポートできない場面や、外部ライブラリのクラスを扱う場面では、InstanceType でインスタンスの型だけを取り出して使い回すと便利です。たとえば、あるクラスのインスタンスを配列で保持したり、関数の引数として受け取ったりするときに、型エイリアスとして切り出しておけます。

repository.ts
// インスタンスの型に名前を付けておく
type UserModel = InstanceType<typeof User>;

// 取り出した型を引数や戻り値で使い回す
function greet(user: UserModel): string {
  return `こんにちは、${user.name}さん`;
}

const users: UserModel[] = [
  new User("太郎", 20),
  new User("花子", 25),
];

クラスがそのまま型として使える場面では function greet(user: User) と書いても同じ結果になります。InstanceType が本領を発揮するのは、クラス本体(値)は手元にあるが、その型に直接名前でアクセスしづらいケースや、後述するジェネリックなユーティリティを書くケースです。

2つのユーティリティ型の対応

ここまで見てきた2つのユーティリティ型が、それぞれクラスから何を取り出すのかを整理します。どちらもコンストラクタ型(typeof クラス名)を受け取る点は共通です。

ユーティリティ型受け取るもの取り出すもの
InstanceType<T>コンストラクタ型(typeof Userインスタンスの型(例: { name: string; age: number }
ConstructorParameters<T>コンストラクタ型(typeof Userコンストラクタ引数のタプル(例: [string, number]

これらは特別な魔法ではなく、Conditional Types(条件型)と infer を組み合わせて標準ライブラリ内で定義されています。イメージとしては、次のように「new で呼べる関数型かどうかを判定し、その戻り値や引数を infer で抜き出す」という形です。

lib.es5.d.ts(定義のイメージ)
type InstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : any;

type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

infer R でコンストラクタの戻り値(=インスタンスの型)を、infer P で引数の並び(=タプル)を取り出しているのが分かります。仕組みを丸暗記する必要はありませんが、「条件型と infer で実装されている」と知っておくと、独自の型ユーティリティを書きたくなったときの応用が利きます。

typeof の付け忘れと abstract クラスの挙動

typeof を付け忘れるとエラーになる

もっとも多いミスが、typeof を付け忘れて InstanceType<User> と書いてしまうケースです。前述のとおり、型の位置の User はインスタンスの型であり、コンストラクタ型ではありません。InstanceType は「new で呼べる型」を要求するため、インスタンス型を渡すと制約を満たさずエラーになります。

user.ts
// NG: User はインスタンスの型なので new できない
type Wrong = InstanceType<User>;
// エラー: Type 'User' does not satisfy the constraint
//        'abstract new (...args: any) => any'

// OK: typeof を付けてコンストラクタ型を渡す
type Right = InstanceType<typeof User>;

エラーメッセージに does not satisfy the constraint 'abstract new (...args: any) => any' と出たら、typeof の付け忘れをまず疑ってください。ユーティリティ型に渡すのは「値としてのクラス(コンストラクタ)」だと覚えておけば、この種のエラーは避けられます。

abstract クラスでは InstanceType が使える

組み込み定義の制約が abstract new (...args: any) => any になっていることからも分かるように、InstanceTypeConstructorParametersabstract クラス(抽象クラス)にも使えます。抽象クラスは直接 new できませんが、そのインスタンスの形やコンストラクタ引数の型を取り出すことは問題なく行えます。

shape.ts
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;
}

// 抽象クラスでもインスタンス型・引数型は取り出せる
type ShapeInstance = InstanceType<typeof Shape>;
// → { color: string; area(): number }
type ShapeArgs = ConstructorParameters<typeof Shape>;
// → [color: string]

// ただし new はできない
// const s = new Shape("red"); // エラー: 抽象クラスはインスタンス化できない

InstanceType<typeof Shape> は、抽象メソッド area() を含んだインスタンスの型を返します。型の取り出しはあくまで型レベルの操作なので、実行時に new Shape(...) できないこととは切り離して考えて大丈夫です。抽象クラスを基底にした設計でも、これらのユーティリティ型はそのまま活用できます。

まとめ

InstanceType<T> はコンストラクタ型からインスタンスの型を、ConstructorParameters<T> はコンストラクタ引数の型をタプルとして取り出す、TypeScript の組み込みユーティリティ型です。どちらもクラスの定義から型を自動で導き出すため、クラスを変更しても型が追従し、修正漏れを防げます。使ううえでの要は、クラス名が「型の位置ではインスタンスの型」「値の位置ではコンストラクタ」を指すという二面性を理解し、ユーティリティ型には typeof クラス名 でコンストラクタ型を渡すことです。ファクトリ関数の引数をコンストラクタにそろえたり、インスタンスの型を切り出して使い回したりと応用範囲は広く、abstract クラスに対しても問題なく使えます。typeof の付け忘れによるエラーだけ注意すれば、クラス中心の設計を型安全に保つ強力な道具になります。

参考ページ