Перейти к основному содержимому

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

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

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


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

Эта страница даёт обзор основных продвинутых возможностей системы типов:

  • Отображаемые типы (Mapped Types): преобразуют свойства существующего типа
  • Условные типы (Conditional Types): выбирают тип по условию
  • Шаблонные строковые типы (Template Literal Types): строят строковые типы с помощью шаблонного синтаксиса
  • Служебные типы (Utility Types): встроенные помощники для частых преобразований
  • Рекурсивные типы (Recursive Types): типы, которые ссылаются сами на себя, для древовидных данных
  • Защитники типов и предикаты типов: проверки, которые сужают тип во время выполнения
  • Вывод типов: сопоставление с образцом внутри типов с помощью infer

Отображаемые типы

Отображаемый тип создаёт новый тип: он проходит по свойствам существующего типа и преобразует каждое из них.

Простой отображаемый тип

Один шаблон превращает каждое свойство объектного типа в новый тип. Здесь каждое свойство типа User становится boolean.

Prompt
Покажите простой отображаемый тип, который превращает каждое свойство объектного типа в boolean. Объявите дженерик Flags<T>, отображаемый по keyof T, интерфейс User с полями id: number, name: string и email: string, а также псевдоним UserFlags = Flags<User>; добавьте комментарий, показывающий эквивалентный объектный тип. Файл только объявляет типы и ничего не печатает.
Вывод: программа на TypeScript и захваченный вывод

Модификаторы отображаемых типов

Во время отображения можно добавлять или удалять модификаторы readonly и ? (необязательное свойство) у каждого ключа. Знак + добавляет модификатор, а - удаляет его.

Prompt
Покажите, как отображаемый тип добавляет или удаляет модификаторы readonly и ?. Объявите интерфейс Todo (title: string, description: string, completed: boolean), отображаемый тип OptionalTodo, который делает каждое свойство необязательным, дженерик Concrete<T>, который снимает и readonly, и ? с помощью -readonly и -?, а также ReadonlyRequired<T>, который добавляет +readonly и удаляет ?. Только объявления типов, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

С помощью конструкции as можно переименовывать ключи во время отображения или отфильтровывать их, сопоставляя с never. Это становится возможным благодаря строковым помощникам вроде Capitalize и условным проверкам.

Prompt
Покажите переименование ключей в отображаемом типе с помощью конструкции as. Объявите интерфейс User (id: number, name: string, email: string), дженерик Getters<T>, который переименовывает каждый ключ в get плюс имя ключа с заглавной буквы (используя Capitalize<string & K>) со значением () => T[K], псевдоним UserGetters = Getters<User> с комментарием, показывающим получившуюся форму, и тип MethodsOnly<T>, который оставляет только ключи, чьи значения расширяют Function, сопоставляя остальные с never. Только типы, ничего не печатается.
Вывод: программа на TypeScript и захваченный вывод

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

Условный тип выбирает один из двух типов в зависимости от условия. Он работает как оператор if, но на уровне типов.

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

Форма T extends U ? X : Y выбирает X, когда T совместим с U, и Y в противном случае. Проверка целиком происходит на уровне типов.

Prompt
Покажите простой условный тип и тип, использующий infer. Объявите IsString<T> как T extends string ? true : false, затем псевдонимы A, B, C, D для string, number, литерала 'hello' и string | number с комментариями, отмечающими результаты true, false, true и boolean. Также объявите ArrayElement<T>, который выводит тип элемента U типа массива, и псевдоним Numbers = ArrayElement<number[]>. Только типы, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

Внутри условия условного типа infer вводит новую типовую переменную и захватывает часть проверяемого типа. Именно так работают встроенные помощники ReturnType и Parameters.

Prompt
Покажите, как ключевое слово infer захватывает части типа внутри условного типа, воссоздав четыре встроенных помощника. Объявите ReturnType<T> (выводит возвращаемый тип R типа функции, иначе any), Parameters<T> (выводит кортеж параметров P, иначе never), ConstructorParameters<T>, ограниченный типами конструкторов и выводящий кортеж параметров конструктора, и InstanceType<T>, выводящий тип экземпляра конструктора. Только типы, ничего не печатается; сделайте файл модулем, чтобы имена не конфликтовали со встроенными глобальными.
Вывод: программа на TypeScript и захваченный вывод

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

Когда проверяемый тип — это просто параметр типа, условный тип применяется к каждому члену объединения по отдельности. Это называется распределением. Если обернуть тип в кортеж, как в [T] extends [any], распределение прекращается и объединение обрабатывается как один тип.

Prompt
Покажите, как условные типы распределяются по объединениям и как остановить это распределение. Объявите ToArrayNonDist<T> как T extends any ? T[] : never и StrOrNumArr = ToArrayNonDist<string | number>, затем ToArray<T>, записанный с обёрткой-кортежем [T] extends [any] ? T[] : never, и StrOrNumArr2 = ToArray<string | number>, каждый с комментарием, показывающим получившийся тип. Наконец объявите FilterStrings<T>, который оставляет только строковые члены, и псевдоним Letters, применённый к 'a' | 'b' | 1 | 2 | 'c'. Только типы, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

Шаблонные строковые типы строят новые строковые типы с помощью того же синтаксиса обратных кавычек, что и шаблонные строки JavaScript.

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

Шаблонный строковый тип ограничивает строку определённым образцом. В сочетании с объединениями он порождает все возможные комбинации частей. Присваивание строки, которая не соответствует образцу, — это ошибка компиляции.

Prompt
Покажите простые шаблонные строковые типы и ошибку компиляции, которую они обнаруживают. Объявите тип Greeting, равный шаблону `Hello, ${string}`, присвойте корректное значение 'Hello, World!' переменной validGreeting и присвойте 'Hi there!' переменной invalidGreeting с пометкой // Error, чтобы файл намеренно не прошёл проверку типов. Затем объявите объединения Color ('red' | 'green' | 'blue') и Size ('small' | 'medium' | 'large') и шаблонный тип Style вида `${Color}-${Size}` с комментарием, перечисляющим девять получившихся членов. Ожидаемый результат: ошибка компиляции для некорректного приветствия.
Вывод: программа на TypeScript и захваченный вывод

Типы для работы со строками

В TypeScript есть четыре помощника, которые меняют регистр строковых литеральных типов: Uppercase, Lowercase, Capitalize и Uncapitalize. Они удобны внутри шаблонных строковых типов.

Prompt
Покажите встроенные типы для работы со строками, применённые к строковым литералам. Объявите типы от T1 до T4, используя Uppercase<'hello'>, Lowercase<'WORLD'>, Capitalize<'typescript'> и Uncapitalize<'TypeScript'>, с комментариями, показывающими результаты, затем объединение EventType ('click' | 'change' | 'keydown') и шаблонный тип EventHandler, который добавляет приставку on к именам событий с заглавной буквы. Только типы, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

Шаблонные строковые типы можно сочетать с infer и переименованием ключей. Это позволяет извлекать информацию из строкового типа, например параметры маршрута, или создавать целый набор имён обработчиков из карты событий.

Prompt
Покажите продвинутые приёмы с шаблонными строковыми типами, сочетающие infer и переименование ключей. Объявите рекурсивный тип ExtractRouteParams<T>, который сопоставляется с `${string}:${infer Param}/${infer Rest}` (и с формой для одного параметра), чтобы построить объектный тип с одним свойством string на каждый параметр маршрута, а также псевдоним Params для '/users/:userId/posts/:postId'. Затем объявите тип EventMap (click с числами x/y, change как string, keydown с key: string и code: number) и отображаемый тип EventHandlers, который переименовывает каждый ключ в on плюс имя с заглавной буквы и типизирует его как обработчик, принимающий EventMap[K]. Только типы, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

В TypeScript есть много встроенных служебных типов. Они покрывают самые частые преобразования типов, поэтому вам не нужно писать их самостоятельно.

Распространённые служебные типы

Partial, Required, Readonly, Pick и Omit — те, которыми вы будете пользоваться чаще всего. Также можно получить тип одного свойства через User['id'] и все имена ключей через keyof User.

Prompt
Покажите самые распространённые встроенные служебные типы на примере одного интерфейса. Объявите интерфейс User с полями id: number, name: string, email: string и createdAt: Date, затем псевдонимы PartialUser = Partial<User>, RequiredUser = Required<PartialUser>, ReadonlyUser = Readonly<User>, UserPreview = Pick<User, 'id' | 'name'> и UserWithoutEmail = Omit<User, 'email'>, каждый с коротким комментарием. Завершите типами UserId = User['id'] и UserKeys = keyof User с комментариями, показывающими результаты. Только типы, без вывода.
Вывод: программа на TypeScript и захваченный вывод

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

Exclude убирает члены из объединения, а Extract оставляет только подходящие. Можно также писать собственные помощники, например тип Mutable, который снимает readonly со всех свойств.

Prompt
Покажите служебные типы для фильтрации объединений и собственный отображаемый помощник. Объявите условный тип NonNullable<T>, который сопоставляет null | undefined с never, объединение Numbers вида 1 | 2 | 3 | 'a' | 'b', типы JustNumbers = Exclude<Numbers, string> и JustStrings = Extract<Numbers, string> с комментариями о результатах, затем объектные типы A (a: string; b: number; c: boolean) и B (a: string; b: number) и C = Omit<A, keyof B>, и наконец отображаемый тип Mutable<T>, использующий -readonly. Только типы, без вывода; сделайте файл модулем, чтобы NonNullable не конфликтовал с глобальным типом.
Вывод: программа на TypeScript и захваченный вывод

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

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

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

Узел двоичного дерева содержит необязательные узлы left и right того же типа. Значение JSON может содержать массивы и объекты из других значений JSON. Комментарий содержит массив ответов, которые тоже являются комментариями.

Prompt
Покажите простые рекурсивные типы, которые моделируют древовидные данные. Объявите дженерик BinaryTree<T> со значением value и необязательными поддеревьями left/right того же типа, объединение JSONValue из string, number, boolean, null, JSONValue[] и объекта со значениями JSONValue, а также тип Comment с полями id, content, массивом replies: Comment[] и createdAt: Date. Только типы, без вывода; сделайте файл модулем, чтобы Comment не конфликтовал с глобальным типом DOM.
Вывод: программа на TypeScript и захваченный вывод

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

Та же идея описывает связный список, дерево каталогов, потомки которого — файлы или другие каталоги, конечный автомат, переходы которого ведут к другим состояниям, и даже тип функции, которая принимает саму себя.

Prompt
Покажите более продвинутые рекурсивные типы. Объявите дженерик LinkedList<T> с полями value и next: LinkedList<T> | null, тип File (type: 'file', name, size) и тип Directory (type: 'directory', name, children: (File | Directory)[]), тип State, у которого transitions сопоставляет имена событий другим состояниям State, и тип функции RecursiveFunction<T>, которая принимает T | RecursiveFunction<T>. Только типы, без вывода; сделайте файл модулем, чтобы File не конфликтовал с глобальным типом DOM.
Вывод: программа на TypeScript и захваченный вывод

Рекомендации

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

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

Что учитывать для производительности:

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

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

Сложности с выводом типов:

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

Удобство поддержки:

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