Система расширенных или продвинутых типов 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
Поддерживаемость
- Чрезмерное использование сложных типов затрудняет понимание кода
- Документируйте сложные трансформации типов с помощью комментариев
- Используйте утверждения типов или вспомогательных функций для особо сложных случаев