Урок по треку TypeScript 7 мин

Как решать задачи на типы

Пишем утилиты типов сами: от DeepReadonly до разбора строк.

Задачи на типы проверяют, умеете ли вы собрать утилиту из кирпичей: 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] }.
  • Глубина рекурсии ограничена, для длинных строк и кортежей нужна хвостовая рекурсия с накопителем.

Как вести себя на собесе

  1. Проговорите вход и выход на примере.
  2. Назовите приём: «пройдусь mapped-типом» или «разберу кортеж рекурсией».
  3. Напишите базовый случай рекурсии первым.
  4. Проверьте граничные случаи: пустой кортеж, never, объединение, необязательные поля.

Что спросят на собесе

  • Напишите DeepReadonly, Last, TupleToUnion.
  • Как отключить распределение по объединению.
  • Как проверить, что тип равен never.
  • Как разобрать строку 'a.b.c' в кортеж.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.