RequiredByKeys

Challenge

Implement a generic RequiredByKeys<T, K> which takes two type argument T and K.

K specify the set of properties of T that should set to be required. When K is not provided, it should make all properties required just like the normal Required<T>.

For example

interface User {
  name?: string;
  age?: number;
  address?: string;
}

type UserRequiredName = RequiredByKeys<User, "name">; // { name: string; age?: number; address?: string }

Solution

Zur Lösung dieses Problems können wir eine sogenannte Intersection (Schnittmenge) zwischen zwei Objekt-Typen in Kombination mit einem Mapped Type nutzen, um die Schlüssel die ausgewählten Eigenschaften auf required zu setzen. Wir beginnen also damit, einen Mapped Type zu erstellen, der nur aus Eigenschaft / Wert des zweiten Arguments besteht:

// Das User-Interface erhalten wir aus der Aufgabenstellung
type UserPartialName = PartialByKeys<User, "name">;
// PartialByKeys<User, 'name'>  => { 'name': string }
type PartialByKeys<T, K = keyof T> = {
  [Key in keyof T as Key extends K ? Key : never]-?: T[Key];
};

Um nun das richtige Ergebnis zu erhalten, müssen wir diesen neuen Typ mit einem Typ zusammenführen, aus dem der Schlüssel entfernt wurde (z.B. mittels Omit). Jetzt müssen wir noch mittels eines weiteren Hilfstypen in einen Mapped Type umwandeln, um die Schnittmenge als normalen Objekt-Typ darzustellen.

// Beispiel: PartialByKeys<User, 'name'>
type MyOmit<T, K> = { [Key in keyof T as Key extends K ? never : Key]: T[Key] };
type PartialByKeys<T, K = keyof T> = MergeObjects<
  { [Key in keyof T as Key extends K ? Key : never]?: T[Key] } & MyOmit<T, K>
>;

// Zwischenergbnis: { 'name': string } & MyOmit<User, 'name'>

type MergeObjects<T> = { [Key in keyof T]: T[Key] };
// => nach MergeObject: { 'name': string,  age?: number | undefined, address?: string | undefined }

References