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

Условные и mapped-типы

extends ? :, infer, распределение по объединению, keyof и переназначение ключей.

Это «программирование на типах»: условия, циклы по ключам и сопоставление с образцом. На этом построены все утилитарные типы.

keyof, typeof, индексный доступ

type User = { id: number; name: string };
type Keys = keyof User;          // 'id' | 'name'
type Name = User['name'];        // string
type Values = User[keyof User];  // number | string

const config = { port: 3000 };
type Config = typeof config;     // { port: number }

const roles = ['admin', 'user'] as const;
type Role = (typeof roles)[number]; // 'admin' | 'user'

Mapped-типы

Проходят по объединению ключей и строят новый объект:

type Flags<T> = { [K in keyof T]: boolean };

Модификаторы: readonly и ? можно добавить (+, по умолчанию) или снять (-):

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };

Mapped-тип вида { [K in keyof T]: ... } гомоморфный: он сохраняет ? и readonly исходника, а для массивов и кортежей возвращает массив или кортеж.

Переназначение ключей через as

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type G = Getters<User>; // { getId: () => number; getName: () => string }

type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

Ключ, переназначенный в never, выбрасывается: так фильтруют поля.

Условные типы

type IsString<T> = T extends string ? true : false;

Если T это «голый» параметр, а на вход пришло объединение, условие применяется к каждому члену и результаты объединяются (дистрибутивность):

type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]

Чтобы отключить распределение, оберните обе стороны в кортеж:

type ToArrayOne<T> = [T] extends [any] ? T[] : never;
type B = ToArrayOne<string | number>; // (string | number)[]

Особый случай: never это пустое объединение, поэтому IsString<never> даёт never, а не false.

infer

infer объявляет переменную типа внутри условия и вытаскивает часть:

type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Ret<T> = T extends (...args: any[]) => infer R ? R : never;
type First<T> = T extends [infer F, ...unknown[]] ? F : never;

Шаблонные литеральные типы

type Event = 'click' | 'focus';
type Handler = `on${Capitalize<Event>}`; // 'onClick' | 'onFocus'

type Route = `/users/${number}`;

Объединения в шаблоне перемножаются: `${'a'|'b'}-${'x'|'y'}` даёт четыре строки.

Рекурсия

Типы могут ссылаться на себя:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

У рекурсии есть предел глубины, а слишком большие объединения дают ошибку «Expression produces a union type that is too complex».

Частые ошибки

  • Ожидать false от IsString<string | number>: из-за распределения получится boolean, то есть true | false.
  • Забыть string & K в шаблоне: keyof T содержит и number | symbol.
  • Писать T extends object в DeepReadonly и удивляться, что функции тоже «заморожены» как объекты.

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

  • Что такое дистрибутивный условный тип и как его отключить.
  • Для чего infer, напишите ReturnType.
  • Как mapped-тип снимает readonly и ?.
  • Как отфильтровать поля объекта по типу значения.
  • Как получить объединение значений массива as const.

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

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

Вопросы модуля (42)