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];
};
Имея дистрибутивные сопоставляющие типы, мы смогли реализовать очень даже читаемое и поддерживаемое решение. Без них, мы были бы вынуждены использовать дистрибутивные условные типы с дальнейшим применением сопоставляющих внутри условного типа. Что, очевидно, не так хорошо смотрится, как это решение.
Комментарии