🏛️

TypeScriptでClassの初期化をもっと楽にする

2024/07/17に公開

はじめに

TypeScriptでClassを定義する場合、みなさんはどのように初期化を行っていますか?

コンストラクタに引数を並べる方法が一般的だと思いますが、引数が多い場合や、引数の順番を間違えるとバグの原因になります。

class User {
  public readonly id: number;
  public readonly firstName: string;
  public readonly lastName: string;
  
  public constructor(id: number, firstName: string, lastName: string) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

const user = new User(1, 'Taro', 'Yamada');

下記のように、引数をオブジェクトにまとめる方法もありますね。

class User {
  public readonly id: number;
  public readonly firstName: string;
  public readonly lastName: string;

  public constructor(user: User) {
    this.id = user.id;
    this.firstName = user.firstName;
    this.lastName = user.lastName;
  }
}

const user = new User({ id: 1, firstName: 'Taro', lastName: 'Yamada' });

しかし、この方法だとメソッドがある場合に型エラーが発生します。

class User {
  public readonly id: number;
  public readonly firstName: string;
  public readonly lastName: string;

  public constructor(user: User) {
    this.id = user.id;
    this.firstName = user.firstName;
    this.lastName = user.lastName;
  }
  
  public fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new User({ id: 1, firstName: 'Taro', lastName: 'Yamada' });
// Property 'fullName' is missing in type

この記事では、これらの問題を解決しつつ、Classの初期化をもっと楽にする方法を紹介します。

動的Class定義を活用する

JavaScriptでは、下記のように関数内部でClassを定義して返すことができます。

const factory = () => {
  abstract class BaseClass {
      
  }
  return BaseClass;
}

class SubClass extends factory() {
  
}

この特性を利用したのが、下記のStruct関数です。

const Struct = <Properties extends Record<string, unknown>>(): new(
  properties: Properties,
) => Readonly<Properties> => {
  abstract class Class {
    protected constructor(properties: Properties) {
      Object.assign(this, properties);
    }
  }

  return Class as any;
};

Struct関数は、Genericsにプロパティを受け取り、そのプロパティを持つClassを返す関数です。
Constructorは、引数で受け取ったプロパティをObject.assignで自身にコピーします。

使い方

Struct関数を使うと、下記のようにClassを定義できます。

class User extends Struct<{
  id: number;
  firstName: string;
  lastName: string;
}>() {
  public fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

const user = new User({ id: 1, firstName: 'Taro', lastName: 'Yamada' });

UserClassはメソッドを持っていますが、型エラーは発生せず、初期化も簡単に行えます。
プロパティの割り当ては、Struct関数が生成するClassが行ってくれるため、Constructorを定義する必要がありません。

まとめ

ModelやDTOを定義する際には、今回紹介したStruct関数を使ってみてください。
引数の順番を気にする必要がなくなり、Classの定義も楽に行えます。

PrAha

Discussion