Детальный анализ сложного типа в TypeScript

Содержание
Введение
В этом руководстве рассматривается сложный тип в TypeScript, который объединяет несколько продвинутых концепций системы типов.
Анализируется следующий пример:
type Primitive = string | number | boolean | null | undefined type ExtractPrimitives<T> = T extends Primitive ? T : never type UserData = { id: number name: string registered: boolean metadata: object } type PrimitiveUserData = { [K in keyof UserData]: ExtractPrimitives<UserData[K]> }[keyof UserData] // Результат: number | string | boolean (object отфильтрован)
Почему в результате получается number | string | boolean?
Подробное объяснение маппированного типа
Этот тип позволяет извлечь все примитивные типы из объекта UserData
, отфильтровав непримитивные типы, такие как object
. Рассмотрим этот механизм шаг за шагом.
Исходные типы
Для начала, разберем базовые компоненты нашего типа:
Primitive - объединение (union) примитивных типов в TypeScript:
type Primitive = string | number | boolean | null | undefined
ExtractPrimitives - условный тип (conditional type), который работает как фильтр:
type ExtractPrimitives<T> = T extends Primitive ? T : never
Он проверяет: если T
является подтипом Primitive
, то возвращает T
, иначе возвращает never
.
UserData - обычный объектный тип с различными свойствами:
type UserData = { id: number name: string registered: boolean metadata: object }
Здесь свойства id
, name
и registered
имеют примитивные типы, а metadata - непримитивный тип object
.
Маппированный тип: первый этап разбора
Теперь разберем первую часть нашего сложного типа:
{ [K in keyof UserData]: ExtractPrimitives<UserData[K]> }
Запись
[K in keyof UserData]: ExtractPrimitives<UserData[K]>
- это пример маппированного типа в TypeScript, который многим разработчикам JavaScript кажется странным, так как в JavaScript нет подобного синтаксиса. Разберем пошагово, как работает эта запись:
[K in keyof UserData] - это начало создания маппированного типа. Можно представить это как цикл, который проходит по каждому ключу из объекта UserData. K - это переменная цикла, которая принимает значение каждого ключа по очереди.
Здесь происходит следующее:
keyof UserData
дает union-тип всех ключей объекта:"id" | "name" | "registered" | "metadata"
[K in keyof UserData]
создает маппированный тип, который проходит по каждому ключу из объекта UserData- Для каждого ключа K применяется функция типа
ExtractPrimitives<UserData[K]>
, которая проверяет, является ли тип свойства примитивным
Если расписать промежуточный результат этого маппированного типа, получится:
{ id: ExtractPrimitives<number>, // number extends Primitive ? number : never => number name: ExtractPrimitives<string>, // string extends Primitive ? string : never => string registered: ExtractPrimitives<boolean>, // boolean extends Primitive ? boolean : never => boolean metadata: ExtractPrimitives<object> // object extends Primitive ? object : never => never }
После вычисления условных типов, результат:
{ id: number, name: string, registered: boolean, metadata: never // object не является примитивом, поэтому получаем never }
Индексированный доступ: второй этап разбора
Следующий шаг - индексированный доступ к типу с помощью [keyof UserData]
:
{ id: number, name: string, registered: boolean, metadata: never }[keyof UserData]
Это эквивалентно:
{ id: number, name: string, registered: boolean, metadata: never }["id" | "name" | "registered" | "metadata"]
Согласно правилам TypeScript, такой индексированный доступ возвращает объединение (union) всех возможных типов значений, которые можно получить при обращении по каждому ключу из union-типа ключей:
number | string | boolean | never
Финальный результат
В TypeScript тип never
в объединении (union) типов исчезает, поэтому финальный результат:
number | string | boolean | never
Таким образом, успешно извлечены все примитивные типы из объекта UserData и отфильтрованы непримитивные.
Практическое применение
Такой подход к конструированию типов имеет множество практических применений:
- Фильтрация типов - извлечение подмножества типов из сложных структур данных по определенному критерию
- Преобразование данных - создание новых структур типов на основе существующих
- Валидация типов - проверка соответствия данных определенным ограничениям на этапе компиляции
- Создание утилитарных типов - разработка переиспользуемых утилит для работы с типами
- Интеграция с API - преобразование типов данных, полученных из внешних источников, в более удобный формат
Глубокое понимание индексного доступа
Запись вида SomeObjectType[keyof SomeObjectType]
является мощным паттерном в TypeScript. По сути, это операция "дай мне все возможные типы значений, которые существуют в этом объектном типе".
Она особенно полезна, когда необходимо:
- Получить union всех возможных типов значений в объекте
- Создать новые типы на основе свойств существующих типов
- Выполнить выборочную фильтрацию типов с помощью предварительных преобразований
Такие продвинутые техники типизации позволяют создавать более безопасный, самодокументируемый и поддерживаемый код, полностью используя мощь системы типов TypeScript.
Оригинальный текст статьи gist.github.com/olegopro