iTranslated by AI
Making Class Initialization as Easy as Possible
For those who find it a hassle to read:
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