🥺

Classの初期化を出来るだけ楽にする

2022/05/09に公開

読むのがめんどくさい人はこちら
https://www.typescriptlang.org/play?#code/C4TwDgpgBAggdiAYgVzgY2ASwPZygXigAoA6MgQwCcBzAZwC4pyEBtAXQEoCA+JhAbgCwAKBGhIUANIQQtAPIAzACrgIAHiUAaKAGVehAN4ioUFgGsZUTHgshsCqEraMdUCAA9gEOABNaj8xk2KAB+KFsoRjgIADcISiFhAF9AuwcnRLFVKABVXwgFawgfJWw5MCxccgAbDX1jKDkAW0xgDW1pWUUVSHaoVB8Cop9uXgAyBoAFKiwatUnMNDM+zvllVT6BoeiR0czhcWhETAhqvzqCXPzCndLyyrg55taVmTWe9S1YBBR0B9HuPs0NVyLR-DlaPEoEZhCYwMgAEbVRZQSgQcg+XDVEBWHwAQiiyCaCPiiThiORaFR6MxcGxUEeTQgBKgtGAlGs1DJUHhSJRaIxWJx5GozMJxNJDV5lOpgrpOIgTXImGqIUYbI5cC5IgaaFwGuQGGwlCIYEo2DADCgx1O5wh8W4XBhJhMcgRACsIBgSKDaJhqHAiMAABaYWjaM0W2gcBpJERx0TCaIAd1ykJNztxjAADJoGoyIIwAOSABldALRyRbzsKYosYAEYABxVpIcfYptPxIiZzA+HNVkwF4vlysNEWFqCN-tuJUq4vAZjkMzkAACHnITTA1QgJD1TRHyVbIiAA

Classの初期化って面倒くさい

class User {
  public readonly id: number;
  public readonly name: string;
  public readonly age: number;
  public readonly email?: string;

  constructor(
    id: number,
    name: string,
    age: number,
    email?: string,
  ) {
    this.id = id;
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

Classに必要な初期化処理を愚直に毎回書くとめちゃくちゃめんどくさい。
上記のような処理を毎回書くのは骨が折れる。

ちょっと楽にする

class User {
 constructor(
   public readonly id: number,
   public readonly name: string,
   public readonly age: number,
   public readonly email?: string,
 ) {}
}

実はコンストラクタにフィールドを定義する事が出来る。
これだけでも記述量が半分になって楽になる。

まだ面倒な事が・・・

クラスの記述は楽になったけど、生成処理はどうだろうか。

const user = new User(1, '田中', 18, 'tanaka@example.com');

引数の順番覚えるのめんどくさい・・・
何番目がどのプロパティになるんだっけ・・・?
これプロパティ増えてきたらやばくない?

もっと楽にする

type AnyFunction = (...args: any[]) => any;

type KeysOfType<T, S> = {
  [key in keyof T]: S extends T[key] ? key : never;
}[keyof T];

type UndefinedToOptional<T> =
  Omit<T, KeysOfType<T, undefined>> &
  Partial<Pick<T, KeysOfType<T, undefined>>>;

type Fields<T> = UndefinedToOptional<Omit<T, KeysOfType<T, AnyFunction>>>;

TypeScriptの型パズルパワーを借りて楽にしよう。
クラス定義は下記のように変更する。

class User {
  public readonly id!: number;
  public readonly name!: string;
  public readonly age!: number;
  public readonly email?: string;

  constructor(props: Fields<User>) {
    Object.assign(this, props)
  }
}

ちょっとだけ記述量は増えたかな。
生成処理はどうだろう?

const user = new User({
  id: 1,
  name: '田中',
  age: 18,
  email: 'tanaka@example.com',
});

プロパティを指定するようになったから可読性が高くなった気がするね。
これなら今後、プロパティが増えても大丈夫かな。

型定義解説

AnyFunction

type AnyFunction = (...args: any[]) => any;

何らかの関数を表現する型定義。

const func1: AnyFunction = () => {};
const func2: AnyFunction = (id: number) => {};
const func3: AnyFunction = () => 1;

上記のように関数であれば適合する事が出来る。

KeysOfType

type KeysOfType = {
  [key in keyof T]: S extends T[key] ? key : never;
}[keyof T];

Objectから該当する型のキーを取得する型定義。
ConditionalTypesを使用して、指定された型に適合するキー名だけを抽出している。

type Keys = KeysOfType<{
  id: number;
  name: string;
  email: string;
}, string>;

const key1: Keys = 'name';
const key2: Keys = 'email';

上記の場合Keys型はnameemailのTuple型になる。

UndefinedToOptional

type UndefinedToOptional<T> =
  Omit<T, KeysOfType<T, undefined>> &
  Partial<Pick<T, KeysOfType<T, undefined>>>;

undefinedなプロパティをOptionalに変更する型定義。
Objectからundefinedを含まないプロパティを抽出した物と、undefinedを含むプロパティを抽出しPartialでラップした物を合成する事でundefinedをOptionalに変更している。

type User = {
  id: number;
  name: string;
  email: string | undefined;
};

const user1: User1 = {
  id: 1,
  name: '田中',
  emai: undefined,
};

const user2: UndefinedToOptional<User2> = {
  id: 1,
  name: '田中',
};

上記のようにUndefinedToOptionalでラップする事により、undefinedなプロパティを省略する事が出来る。

Fields

type Fields<T> = UndefinedToOptional<Omit<T, KeysOfType<T, AnyFunction>>>;

クラスから初期化に必要なフィールドを抽出する型定義。
クラスにはプロパティ以外にもメソッドが生えている場合があるので、KeysOfTypeAnyFunctionを利用して関数型を除くプロパティのみを取得するようにしている。

Discussion