TypeScript Расширенные типы

Система расширенных или продвинутых типов TypeScript позволяет точно моделировать сложные отношения между типами.

Эти возможности особенно полезны для построения надёжных, легко поддерживаемых приложений с отличной защитой типов.

Ключевые особенности продвинутых типов

  • Преобразованные типы: трансформируют свойства существующих типов
  • Условные типы: создают типы на основе условий
  • Шаблонные строковые типы: типы построенные с использованием строковых шаблонов
  • Служебные типы: встроенные помощники для общих трансформаций
  • Рекурсивные типы: самоссылочные типы для древовидных структур
  • Защитники типов и предикаты типов: проверка типов во время выполнения
  • Вывод типов: расширенная работа с шаблонами через ключевое слово infer

Преобразованные типы

Преобразованные типы позволяет создавать новые типы путём трансформации свойств существующих типов.

Базовый преобразованный тип

Трансформация каждого свойства объектного типа в новый тип с помощью единого шаблона.

Пример


// Преобразует все свойства в булевые значения
type Flags<T> = {
  [K in keyof T]: boolean;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserFlags = Flags<User>;
// Эквивалентно:
// {
//   id: boolean;
//   name: boolean;
//   email: boolean;
// }

Модификаторы преобразованного типа

Добавление или удаление модификаторов свойств (таких как readonly и ?) ко всем ключам.

Пример


// Сделать все свойства необязательными
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type OptionalTodo = {
  [K in keyof Todo]?: Todo[K];
};

// Удалить модификаторы 'readonly' и '?'
type Concrete<T> = {
  -readonly [K in keyof T]-?: T[K];
};

// Добавить модификаторы 'readonly' и обязательность ко всем свойствам
type ReadonlyRequired<T> = {
  +readonly [K in keyof T]-?: T[K];
};

Переименование ключей

Изменение или фильтрация ключей при сопоставлении при помощи оператора as, строковых манипуляторов и условных выражений.

Пример


// Добавляем префикс ко всем именам свойств
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

// Фильтруем свойства
type MethodsOnly<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};


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

Условные типы позволяют определять типы, зависящие от условия.

Базовые условные типы

Выбор между типами на основании условия, проверяемого на уровне типов.

Пример


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

type A = IsString<string>;    // true
type B = IsString<number>;    // false
type C = IsString<'hello'>;    // true
type D = IsString<string | number>; // boolean

// Извлекаем тип элемента массива
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number

Ключевое слово infer

Захват части типа внутри условного типа путём введения новой типовой переменной с помощью infer.

Пример


// Получаем тип возврата функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

// Получаем типы параметров как кортеж
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Получаем типы параметров конструктора
type ConstructorParameters<T extends new (...args: any) => any> =
  T extends new (...args: infer P) => any ? P : never;

// Получаем тип экземпляра из конструктора
type InstanceType<T extends new (...args: any) => any> =
  T extends new (...args: any) => infer R ? R : any;

Распределённые условные типы

Понимание того, как условные операторы распределяются по объединениям против того как они оборачиваются, чтобы предотвратить распределение.

Пример


// Без распределения
type ToArrayNonDist<T> = T extends any ? T[] : never;
type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[]

// С распределением
type ToArray<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArray<string | number>; // string[] | number[]

// Фильтрование нестроковых типов
type FilterStrings<T> = T extends string ? T : never;
type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c'


Шаблонные строковые типы

Шаблонные строковые типы позволяют строить типы с использованием шаблонного синтаксиса.

Базовые шаблонные строковые типы

Ограничение строк конкретными паттернами с помощью шаблонных литералов и объединений.

Пример


type Greeting = `Hello, ${string}`;

const validGreeting: Greeting = 'Hello, World!';
const invalidGreeting: Greeting = 'Hi there!'; // Ошибка

// С объединениями
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';

type Style = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' |
// 'green-small' | 'green-medium' | 'green-large' |
// 'blue-small' | 'blue-medium' | 'blue-large'

Манипуляции со строковыми типами

Применяйте встроенные средства манипуляции строковыми литеральными типами (заглавные буквы, капитализация и т.п.).

Пример


// Встроенные операции над строками
type T1 = Uppercase<'hello'>;  // 'HELLO'
type T2 = Lowercase<'WORLD'>;  // 'world'
type T3 = Capitalize<'typescript'>;  // 'Typescript'
type T4 = Uncapitalize<'TypeScript'>;  // 'typeScript'

// Создание типа обработчика событий
type EventType = 'click' | 'change' | 'keydown';
type EventHandler = `on${Capitalize<EventType>}`;
// 'onClick' | 'onChange' | 'onKeydown'

Продвинутые паттерны

Компонуйте шаблоны с выведением и переименованием ключей для извлечения метаданных и генерации API.

Пример


// Извлечение параметров маршрута
type ExtractRouteParams<T> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<`${Rest}`>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }

// Создание безопасной системы событий
type EventMap = {
  click: { x: number; y: number };
  change: string;
  keydown: { key: string; code: number };
};

type EventHandlers = {
  [K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};


Служебные типы

TypeScript предоставляет ряд встроенных служебных типов для стандартных трансформаций типов.

Общие служебные типы

Используйте встроенные типы, такие как Partial, Pick и Omit, для частых преобразований.

Пример


// Базовые типы
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// Сделаем все свойства необязательными
type PartialUser = Partial<User>;

// сделаем все свойства обязательными
type RequiredUser = Required<PartialUser>;

// делаем все свойства неизменяемыми
type ReadonlyUser = Readonly<User>;

// выбираем конкретные свойства
type UserPreview = Pick<User, 'id' | 'name'>;

// исключаем конкретные свойства
type UserWithoutEmail = Omit<User, 'email'>;

// извлекаем типы свойств
type UserId = User['id']; // number
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt'

Продвинутые служебные типы

Исключайте или извлекайте члены из объединений и создавайте собственные преобразованные вспомогательные типы.

Пример


// Создать тип, исключающий null и undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// Исключить типы из объединения
type Numbers = 1 | 2 | 3 | 'a' | 'b';
type JustNumbers = Exclude<Numbers, string>; // 1 | 2 | 3

// Извлечь типы из объединения
type JustStrings = Extract<Numbers, string>; // 'a' | 'b'

// Получить тип, которого нет во втором типе
type A = { a: string; b: number; c: boolean };
type B = { a: string; b: number };
type C = Omit<A, keyof B>; // { c: boolean }

// Создать тип, в котором все свойства будут изменяемыми
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};


Рекурсивные типы

Рекурсивные типы полезны для моделирования древовидных структур данных, где тип может ссылаться на себя.

Базовый рекурсивный тип

Моделирование самоописательных структур, таких как деревья и вложенный JSON.

Пример


// Простое бинарное дерево
type BinaryTree<T> = {
  value: T;
  left?: BinaryTree<T>;
  right?: BinaryTree<T>;
};

// Подобие структуры JSON
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// Вложенные комментарии
type Comment = {
  id: number;
  content: string;
  replies: Comment[];
  createdAt: Date;
};

Продвинутые рекурсивные типы

Представляют связанные списки, иерархии директорий и рекурсивные конечные автоматы.

Пример


// Тип для связанного списка
type LinkedList<T> = {
  value: T;
  next: LinkedList<T> | null;
};

// Тип для структуры директории
type File = {
  type: 'file';
  name: string;
  size: number;
};

type Directory = {
  type: 'directory';
  name: string;
  children: (File | Directory)[];
};

// Тип для конечного автомата
type State = {
  value: string;
  transitions: {
    [event: string]: State;
  };
};

// Тип для рекурсивной функции
type RecursiveFunction<T> = (x: T | RecursiveFunction<T>) => void;


Лучшая практика

Когда использовать продвинутые типы

  • Используйте преобразованные типы, когда вам нужно трансформировать несколько свойств объектного типа
  • Используйте условные типы, когда ваш тип зависит от другого типа
  • Используйте шаблонные строковые типы для манипулирования строками и сопоставления паттернов
  • Используйте служебные типы для стандартных преобразований (по возможности всегда используйте встроенные служебные типы)
  • Используйте рекурсивные типы для древовидных или вложенных структур данных

Вопросы производительности

  • Глубоко вложенные рекурсивные типы замедляют работу компилятора TypeScript
  • Очень большие объединения типов (более 100 членов) вызывают проблемы с производительностью
  • Разделяйте сложные типы с помощью псевдонимов типов

Типичные трудности

Проблемы с выводом типов

  • Условные типы распространяются по объединённым типам, что иногда неожиданно
  • Вывод типов с ключевым словом infer в разных контекстах работает по-разному
  • Некоторые служебные типы плохо взаимодействуют с типами any и unknown

Поддерживаемость

  • Чрезмерное использование сложных типов затрудняет понимание кода
  • Документируйте сложные трансформации типов с помощью комментариев
  • Используйте утверждения типов или вспомогательных функций для особо сложных случаев