iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🥺

Making Class Initialization as Easy as Possible

に公開
2024/07/17

For those who find it a hassle to read:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAggdiAYgVzgY2ASwPZygXigAoA6MgQwCcBzAZwC4pyEBtAXQEoCA+JhAbgCwAKBGhIUANIQQtAPIAzACrgIAHiUAaKAGVehAN4ioUFgGsZUTHgshsCqEraMdUCAA9gEOABNaj8xk2KAB+KFsoRjgIADcISiFhAF9AuwcnRLFVKABVXwgFawgfJWw5MCxccgAbDX1jKDkAW0xgDW1pWUUVSHaoVB8Cop9uXgAyBoAFKiwatUnMNDM+zvllVT6BoeiR0czhcWhETAhqvzqCXPzCndLyyrg55taVmTWe9S1YBBR0B9HuPs0NVyLR-DlaPEoEZhCYwMgAEbVRZQSgQcg+XDVEBWHwAQiiyCaCPiiThiORaFR6MxcGxUEeTQgBKgtGAlGs1DJUHhSJRaIxWJx5GozMJxNJDV5lOpgrpOIgTXImGqIUYbI5cC5IgaaFwGuQGGwlCIYEo2DADCgx1O5wh8W4XBhJhMcgRACsIBgSKDaJhqHAiMAABaYWjaM0W2gcBpJERx0TCaIAd1ykJNztxjAADJoGoyIIwAOSABldALRyRbzsKYosYAEYABxVpIcfYptPxIiZzA+HNVkwF4vlysNEWFqCN-tuJUq4vAZjkMzkAACHnITTA1QgJD1TRHyVbIiAA

Class Initialization is Such a Hassle

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;
  }
}

Writing the necessary initialization logic for a class straightforwardly every time is incredibly tedious.
Writing the process shown above over and over again is a real pain.

Making it a Little Easier

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

Actually, you can define fields directly in the constructor.
Just doing this reduces the amount of code by half and makes it much easier.

Still some tedious things...

The class definition has become easier, but how about the instantiation?

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

It's a hassle to remember the order of arguments...
Which property corresponds to which position...?
Won't this get difficult as more properties are added?

Making it even easier

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>>>;

Let's make it easier by leveraging the power of TypeScript's type puzzles.
Modify the class definition as follows:

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)
  }
}

The amount of code might have increased slightly.
How about the instantiation?

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

Readability feels higher now that we are specifying the properties.
This should be fine even if properties increase in the future.

Type Definition Explanation

AnyFunction

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

A type definition that represents any function.

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

As shown above, it can match any function.

KeysOfType

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

A type definition to retrieve keys of a specific type from an object.
It uses Conditional Types to extract only the key names that match the specified type.

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

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

In the case above, the Keys type becomes a union type of name and email.

UndefinedToOptional

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

A type definition that converts properties that can be undefined into optional properties.
By merging the properties that do not include undefined extracted from the object with the properties that do include undefined wrapped in Partial, undefined is converted to optional.

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

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

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

By wrapping a type with UndefinedToOptional as shown above, you can omit properties that can be undefined.

Fields

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

A type definition to extract the fields required for initialization from a class.
Since classes may have methods in addition to properties, KeysOfType and AnyFunction are used to ensure only properties (excluding function types) are retrieved.

Discussion