Завдання

Реалізуйте MapTypes<T, R>, який перетворить типи в об’єкті T на інші типи, визначені типом R, який має таку структуру:

type StringToNumber = {
  mapFrom: string; // value of key which value is string
  mapTo: number; // will be transformed for number
};

Наприклад:

type StringToNumber = { mapFrom: string; mapTo: number };
MapTypes<{ iWillBeANumberOneDay: string }, StringToNumber>; // gives { iWillBeANumberOneDay: number; }

Майте на увазі, що користувач може надати об’єднання типів:

type StringToNumber = { mapFrom: string; mapTo: number };
type StringToDate = { mapFrom: string; mapTo: Date };
MapTypes<{ iWillBeNumberOrDate: string }, StringToDate | StringToNumber>; // gives { iWillBeNumberOrDate: number | Date; }

Якщо тип не існує у нашій мапі, залиште його як є:

type StringToNumber = { mapFrom: string; mapTo: number };
MapTypes<
  { iWillBeANumberOneDay: string; iWillStayTheSame: Function },
  StringToNumber
>; // // gives { iWillBeANumberOneDay: number, iWillStayTheSame: Function }

Розв’язок

Якщо типи об’єктів у завданні, то на допомогу приходять типи зіставлення! Нам потрібно перебрати властивості об’єкту і перевизначити типи їхніх значень.

Почнемо з порожнього типу, який нам потрібно реалізувати:

type MapTypes<T, R> = any;

Тип-параметр T містить тип об’єкта, який нам потрібно обробити, а R містить правила трансформації типів. Давайте визначимо інтерфейс трансформації як обмеження над дженериком R:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = any;

Перш ніж робити якісь перетворення, давайте почнемо з простого типу зіставлення, який копіює вхідний T:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P];
};

Тепер, маючи тип, який “копіює” тип об’єкта, ми можемо почати додавати туди якісь перетворення. Спочатку давайте перевіримо, чи тип значення збігається з типом mapFrom відповідно до специфікації завдання:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P] extends R["mapFrom"] ? never : never;
};

Якщо вони збігаються, це означає, що нам потрібно замінити цей тип на тип із mapTo:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P] extends R["mapFrom"] ? R["mapTo"] : never;
};

В іншому випадку, згідно зі специфікацією нам потрібно залишити тип як є:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P] extends R["mapFrom"] ? R["mapTo"] : T[P];
};

На даний момент ми проходимо всі тести, окрім того, що стосується об’єднання. У специфікації завдання зазначено, що існує можливість вказати правила трансформації як об’єднання об’єктів.

Отже, нам потрібно перебрати правила трансформації теж. Для цього ми можемо почати із заміни R['mapTo'] на умовний тип. Умовні типи в TypeScript є дистрибутивними, тобто вони перебиратимуть кожен елемент в об’єднанні. Однак це стосується типу, який стоїть на початку умовного типу. Отже, ми починаємо з тип-параметру R і перевіряємо чи він збігається із типом значення:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P] extends R["mapFrom"]
    ? R extends { mapFrom: T[P] }
      ? never
      : never
    : T[P];
};

Дистрибутивні умовні типи виконуватимуть прохід по R, і якщо в якийсь момент буде збіг із типом значення T[P], ми його повертаємо:

type MapTypes<T, R extends { mapFrom: unknown; mapTo: unknown }> = {
  [P in keyof T]: T[P] extends R["mapFrom"]
    ? R extends { mapFrom: T[P] }
      ? R["mapTo"]
      : never
    : T[P];
};

Посилання