TypeScript | 関数・クラス・ジェネリクス:クラス設計 – ConstructorParameters の利用

TypeScript TypeScript
スポンサーリンク

ゴール:「コンストラクタの引数の型を“そのまま再利用する”感覚をつかむ

ConstructorParameters<T> は一言でいうと、

「コンストラクタ型 T から、引数の型だけをタプルとして抜き出すユーティリティ型」

です。

これが分かると、

  • 「クラスのコンストラクタと同じ引数を受け取る関数」を安全に書く
  • 「コンストラクタの引数をそのまま別の場所で使い回す」
  • InstanceType と組み合わせて「コンストラクタ+インスタンス」を汎用的に扱う

といったことが、かなり気持ちよく書けるようになります。


基本:ConstructorParameters は何をしてくれる型か

まずはシンプルなクラスから

例として、こんなクラスを用意します。

class User {
  constructor(
    public name: string,
    public age: number,
    public isAdmin: boolean
  ) {}
}
TypeScript

このとき、User のコンストラクタの引数は、

  • name: string
  • age: number
  • isAdmin: 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);
TypeScript

UserCtorArgs を使うことで、

  • 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 本の関数で、

  • 引数の数・型は、渡されたクラスのコンストラクタに完全に一致
  • 戻り値の型も、そのクラスのインスタンス型に一致

という、かなりリッチな型安全が実現できます。

「コンストラクタの引数とインスタンスの型を、ジェネリクス+ユーティリティ型でつなぐ」

というのが、ConstructorParametersInstanceType の黄金コンビです。


パターン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); }
TypeScript
function create<C extends new (...args: any[]) => any>(
  Ctor: C,
  ...args: ConstructorParameters<C>
): InstanceType<C> { return new Ctor(...args); }
TypeScript

ここまで書けたら、

ConstructorParameters は“コンストラクタの引数の型を再利用するための道具”なんだな」

という感覚が、かなり自分のものになっているはずです。

タイトルとURLをコピーしました