Задачи на типы проверяют, умеете ли вы собрать утилиту из кирпичей: mapped, условные типы, infer, рекурсия, шаблонные строки. Почти любая задача решается одним из шести приёмов.
Приём 1. Пройти по ключам
Нужно изменить каждое поле объекта: mapped-тип.
type Nullable<T> = { [K in keyof T]: T[K] | null };
Приём 2. Отфильтровать ключи
Поле должно остаться или исчезнуть: as с never.
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
Приём 3. Вытащить часть через infer
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
Приём 4. Рекурсия по кортежу
Кортеж разбирается как список: голова и хвост.
type Reverse<T extends unknown[]> =
T extends [infer H, ...infer Rest] ? [...Reverse<Rest>, H] : [];
type Length<T extends readonly unknown[]> = T['length'];
Числа в типах считают длиной кортежа: чтобы получить N + 1, строят кортеж длины N и добавляют элемент.
Приём 5. Рекурсия по строке
Шаблонный литерал с infer разбирает строку:
type TrimLeft<S extends string> =
S extends ` ${infer R}` ? TrimLeft<R> : S;
type Split<S extends string, D extends string> =
S extends `${infer A}${D}${infer B}` ? [A, ...Split<B, D>] : [S];
type CamelCase<S extends string> =
S extends `${infer A}_${infer B}` ? `${A}${CamelCase<Capitalize<B>>}` : S;
infer A в начале шаблона берёт минимально возможный кусок до первого совпадения разделителя.
Приём 6. Сравнить типы точно
A extends B проверяет совместимость, а не равенство. Для точного равенства используют приём с дженерик-функциями:
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
Он отличает any от unknown и readonly поля от обычных.
Ловушки
- Дистрибутивность:
T extends ...над объединением распадается по членам. Нужен[T] extends [...], чтобы проверить объединение целиком. Так пишутIsNever<T> = [T] extends [never] ? true : false. keyof Tвключаетstring | number | symbol: в шаблонах используйтеK & stringилиExtract<K, string>.- Пересечения в результате показываются некрасиво:
{ a: 1 } & { b: 2 }. Приём «сплющить» объект:type Flat<T> = { [K in keyof T]: T[K] }. - Глубина рекурсии ограничена, для длинных строк и кортежей нужна хвостовая рекурсия с накопителем.
Как вести себя на собесе
- Проговорите вход и выход на примере.
- Назовите приём: «пройдусь mapped-типом» или «разберу кортеж рекурсией».
- Напишите базовый случай рекурсии первым.
- Проверьте граничные случаи: пустой кортеж,
never, объединение, необязательные поля.
Что спросят на собесе
- Напишите
DeepReadonly,Last,TupleToUnion. - Как отключить распределение по объединению.
- Как проверить, что тип равен
never. - Как разобрать строку
'a.b.c'в кортеж.