ゴール:「コンストラクタの引数の型を“そのまま再利用する”感覚をつかむ
ConstructorParameters<T> は一言でいうと、
「コンストラクタ型 T から、引数の型だけをタプルとして抜き出すユーティリティ型」
です。
これが分かると、
- 「クラスのコンストラクタと同じ引数を受け取る関数」を安全に書く
- 「コンストラクタの引数をそのまま別の場所で使い回す」
InstanceTypeと組み合わせて「コンストラクタ+インスタンス」を汎用的に扱う
といったことが、かなり気持ちよく書けるようになります。
基本:ConstructorParameters は何をしてくれる型か
まずはシンプルなクラスから
例として、こんなクラスを用意します。
class User {
constructor(
public name: string,
public age: number,
public isAdmin: boolean
) {}
}
TypeScriptこのとき、User のコンストラクタの引数は、
name: stringage: numberisAdmin: boolean
の 3 つです。
ConstructorParameters<typeof User> を使うと、
この「引数の型の並び」をそのままタプル型として取り出せます。
type UserCtorArgs = ConstructorParameters<typeof User>;
// 結果: [name: string, age: number, isAdmin: boolean]
TypeScriptここでのポイントは、
typeof User は「クラス側(コンストラクタ)の型」ConstructorParameters<typeof User> は「そのコンストラクタの引数の型」
という関係になっていることです。
取り出したタプル型は、そのまま引数に使える
例えば、こんな関数を書けます。
function createUser(...args: UserCtorArgs): User {
return new User(...args);
}
const u = createUser("Taro", 20, false);
TypeScriptUserCtorArgs を使うことで、
createUserの引数は、常にUserのコンストラクタと同じ並び・型になる- 将来
Userのコンストラクタの引数が変わっても、UserCtorArgsを使っている側は自動で追従する
という状態になります。
ここが ConstructorParameters の一番おいしいところです。
重要ポイント:ConstructorParameters の引数は「コンストラクタ型」
なぜ typeof User が必要なのか
ConstructorParameters の定義は、ざっくり言うとこうです。
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never;
TypeScriptつまり、
- T は「コンストラクタ型」でなければならない
- そのコンストラクタの
(...args)の部分をinfer Pで推論して、P を返す
という仕組みです。
だからこそ、クラスに対して使うときは、
ConstructorParameters<typeof User>
TypeScriptのように、「クラスそのもの」ではなく
「クラスの型(コンストラクタ型)」を渡す必要があります。
ここを間違えて ConstructorParameters<User> と書くと、
User は「インスタンスの型」なので、コンストラクタ型ではなくなり、型エラーになります。
「ConstructorParameters の引数は“クラス側”」
というのを、頭にしっかり置いておいてください。
パターン1:コンストラクタと同じ引数を受け取る関数を作る
コンストラクタの“ラッパー関数”を安全に書く
さきほどの User を使って、
「コンストラクタと同じ引数を受け取る関数」を作ってみます。
class User {
constructor(
public name: string,
public age: number,
public isAdmin: boolean
) {}
}
type UserCtorArgs = ConstructorParameters<typeof User>;
function createUser(...args: UserCtorArgs): User {
console.log("User を作成します:", args);
return new User(...args);
}
const u = createUser("Taro", 20, false);
TypeScriptここでのメリットは、
createUserの引数の型を手書きしなくていいUserのコンストラクタの定義が変わったら、自動で追従する- 「コンストラクタと同じ引数を受け取る」という意図がコードから読み取れる
という点です。
もしこれを手書きしていたら、
function createUser(name: string, age: number, isAdmin: boolean): User { ... }
TypeScriptとなり、コンストラクタの変更を見落とすリスクが出てきます。
「コンストラクタの引数の型を“真実のソース”にして、それを他の場所で再利用する」
これが ConstructorParameters の本質的な使い方です。
パターン2:ジェネリクスで「どんなクラスでもコンストラクタ引数を受け取る」
汎用的な create 関数を作る
ConstructorParameters は、ジェネリクスと組み合わせると一気に強くなります。
function createInstance<C extends new (...args: any[]) => any>(
Ctor: C,
...args: ConstructorParameters<C>
): InstanceType<C> {
return new Ctor(...args);
}
TypeScript使い方はこうです。
class User {
constructor(public name: string) {}
}
class Product {
constructor(public id: number, public price: number) {}
}
const u = createInstance(User, "Taro"); // 型: User
const p = createInstance(Product, 1, 1000); // 型: Product
TypeScriptここでの流れを整理すると、
Cは「コンストラクタ型」ConstructorParameters<C>は「そのコンストラクタの引数のタプル型」InstanceType<C>は「そのコンストラクタが返すインスタンスの型」
という関係になっています。
この 1 本の関数で、
- 引数の数・型は、渡されたクラスのコンストラクタに完全に一致
- 戻り値の型も、そのクラスのインスタンス型に一致
という、かなりリッチな型安全が実現できます。
「コンストラクタの引数とインスタンスの型を、ジェネリクス+ユーティリティ型でつなぐ」
というのが、ConstructorParameters と InstanceType の黄金コンビです。
パターン3:クラスの配列から「コンストラクタ引数の型」を取り出す
クラスの集合を扱うときの応用
複数のクラスを配列で持っているケースを考えます。
class Dog {
constructor(public name: string) {}
}
class Cat {
constructor(public name: string, public age: number) {}
}
const animalClasses = [Dog, Cat] as const;
TypeScriptここから、「それぞれのコンストラクタ引数の型」を取り出すこともできます。
type AnimalClassUnion = (typeof animalClasses)[number];
// Dog | Cat のクラス側の型
type DogArgs = ConstructorParameters<typeof Dog>; // [name: string]
type CatArgs = ConstructorParameters<typeof Cat>; // [name: string, age: number]
TypeScriptさらに、ジェネリクスと組み合わせると、
type AnimalArgs<C extends AnimalClassUnion> = ConstructorParameters<C>;
TypeScriptのように、「クラスごとに違う引数の型」を表現できます。
これを使えば、
function createAnimal<C extends AnimalClassUnion>(
Ctor: C,
...args: ConstructorParameters<C>
): InstanceType<C> {
return new Ctor(...args);
}
const d = createAnimal(Dog, "Pochi");
const c = createAnimal(Cat, "Tama", 3);
TypeScriptのように、
「どのクラスを渡しても、そのコンストラクタにぴったり合う引数だけを受け取り、そのインスタンスを返す」
という関数が書けます。
パターン4:ラッパークラスやデコレーターで「元のコンストラクタ引数」を引き継ぐ
元のクラスを包むときに、引数の型をそのまま使いたい
例えば、ログを出しながらインスタンスを作るラッパー関数を考えます。
function withLogging<C extends new (...args: any[]) => any>(Ctor: C) {
return class extends Ctor {
constructor(...args: ConstructorParameters<C>) {
console.log("Creating instance with args:", args);
super(...args);
}
};
}
TypeScript使い方はこうです。
class User {
constructor(public name: string, public age: number) {}
}
const LoggedUser = withLogging(User);
const u = new LoggedUser("Taro", 20);
TypeScriptここで ConstructorParameters<C> を使うことで、
withLoggingに渡されたクラスのコンストラクタ引数の型を、そのまま新しいクラスのコンストラクタに引き継げる- 元のクラスのコンストラクタが変わっても、ラッパー側は自動で追従する
という状態になります。
「元のクラスのコンストラクタ引数を、ラッパー側でも正確に再現する」
というのが、ConstructorParameters のとても実用的な使い方です。
まとめ:ConstructorParameters の利用を自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
ConstructorParameters<T> は、「コンストラクタ型 T から、引数の型のタプルを取り出す」ユーティリティ型。
クラスに対して使うときは、ConstructorParameters<typeof Class> のように、typeof で「クラス側の型(コンストラクタ型)」を渡す必要がある。
これを使うと、
- コンストラクタと同じ引数を受け取る関数を、安全かつ DRY に書ける
- ジェネリクスと組み合わせて、「どんなクラスでも create できる関数」を作れる
- ラッパークラスやデコレーターで、元のコンストラクタ引数をそのまま引き継げる
まずは次の 2 つを、自分の手で書いてみてください。
type Args = ConstructorParameters<typeof User>;
function f(...args: Args): User { return new User(...args); }
TypeScriptfunction create<C extends new (...args: any[]) => any>(
Ctor: C,
...args: ConstructorParameters<C>
): InstanceType<C> { return new Ctor(...args); }
TypeScriptここまで書けたら、
「ConstructorParameters は“コンストラクタの引数の型を再利用するための道具”なんだな」
という感覚が、かなり自分のものになっているはずです。
