ReplaceKeys
Завдання
Реалізувати тип ReplaceKeys
, який замінить ключі в об’єднаннях. Якщо якогось
ключа немає в елементі, просто пропускаємо. Такий тип приймає три аргументи.
Наприклад:
type NodeA = {
type: "A";
name: string;
flag: number;
};
type NodeB = {
type: "B";
id: number;
flag: number;
};
type NodeC = {
type: "C";
name: string;
flag: number;
};
type Nodes = NodeA | NodeB | NodeC;
// would replace name from string to number, replace flag from number to string
type ReplacedNodes = ReplaceKeys<
Nodes,
"name" | "flag",
{ name: number; flag: string }
>;
// would replace name to never
type ReplacedNotExistKeys = ReplaceKeys<Nodes, "name", { aa: number }>;
Розв’язок
У нас на руках об’єднання інтерфейсів й нам потрібно по них ітеруватися. В процесі ітерації, потрібно замінити ключі на необхідні, передані через параметри. Дистрибутивність тут, очевидно, дуже допоможе, як і типи зіставлення.
Я почну з такого невеликого факту, як “типи зіставлення в TypeScript також дистрибутивні”. Тобто, якщо ви напишете тип зіставлення, який приймає об’єднання - поведінка буде дистрибутивною. Таким чином, ми зможемо ітеруватися як по об’єднаннях, так і по ключах самого інтерфейсу одночасно. Давайте спробую пояснити.
Ви вже напевно знаєте, що умовні типи дистрибутивні. Така властивість мови нам
дуже часто допомагала в завданнях раніше. Кожного разу, коли ми писали
U extends any ? U[] : never
, ми отримували всередині правдивої гілки один
елемент з всього об’єднання на кожній ітерації. Компілятор робить це за нас.
Аналогічна властивість присутня і в типах зіставлення. Ми можемо написати тип зіставленні і, якщо на вході об’єднання, компілятор буде неявно застосовувати дистрибутивність. Відповідно, ми будемо працювати з окремим елементом із об’єднання.
Так що давайте почнемо з простого. Візьмемо кожен елемент із U
(дякую
дистрибутивності) і на кожному елементі візьмемо список ключів, разом з його
типами значень.
type ReplaceKeys<U, T, Y> = { [P in keyof U]: U[P] };
Таким чином, ми скопіювали один в один все, що прийшло через параметр U
.
Тепер, нам потрібно застосувати фільтр й працювати тільки з тими ключами, які є
в T
і Y
.
Для початку перевіримо, чи є поточна властивість в параметрі T
(в списку
ключів, які нам потрібно оновити).
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? never : never;
};
Якщо це так, то це означає що розробник попросив нас змінити вказаний ключ. І,
скоріше за все, він вказав і тип, на який потрібно провести заміну. Але, ми не
можемо бути впевненими в цьому. Тому, додатково перевіримо, що ця властивість
присутня в Y
.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? (P extends keyof Y ? never : never) : never;
};
І тільки в ситуації, коли обидві умови правдиві, ми можемо бути впевненими в
необхідності заміни. Беремо тип із Y
й проводимо заміну.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : never;
};
Проте, якщо виявиться, що такого ключа немає у Y
, але він є в T
, нам
потрібно повернути never
(відповідно до завдання). Залишається останній
варіант, коли обидві умови неправдиві. В такому випадку, ми просто повертаємо
той же тип, який був в оригінальному інтерфейсі.
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P];
};
Маючи дистрибутивні типи зіставлення, ми змогли реалізувати дуже зрозуміле рішення. Без них, ми були б змушені використовувати дистрибутивні умовні типи з подальшим застосуванням типів зіставлення всередині умовного типу. Що, очевидно, не так добре виглядає, як це рішення.
Коментарі