Детальный анализ сложного типа в 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 - это переменная цикла, которая принимает значение каждого ключа по очереди.

Здесь происходит следующее:

  1. keyof UserData дает union-тип всех ключей объекта: "id" | "name" | "registered" | "metadata"
  2. [K in keyof UserData] создает маппированный тип, который проходит по каждому ключу из объекта UserData
  3. Для каждого ключа 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 и отфильтрованы непримитивные.

Практическое применение

Такой подход к конструированию типов имеет множество практических применений:

  1. Фильтрация типов - извлечение подмножества типов из сложных структур данных по определенному критерию
  2. Преобразование данных - создание новых структур типов на основе существующих
  3. Валидация типов - проверка соответствия данных определенным ограничениям на этапе компиляции
  4. Создание утилитарных типов - разработка переиспользуемых утилит для работы с типами
  5. Интеграция с API - преобразование типов данных, полученных из внешних источников, в более удобный формат

Глубокое понимание индексного доступа

Запись вида SomeObjectType[keyof SomeObjectType] является мощным паттерном в TypeScript. По сути, это операция "дай мне все возможные типы значений, которые существуют в этом объектном типе".

Она особенно полезна, когда необходимо:

  1. Получить union всех возможных типов значений в объекте
  2. Создать новые типы на основе свойств существующих типов
  3. Выполнить выборочную фильтрацию типов с помощью предварительных преобразований

Такие продвинутые техники типизации позволяют создавать более безопасный, самодокументируемый и поддерживаемый код, полностью используя мощь системы типов TypeScript.

Оригинальный текст статьи gist.github.com/olegopro

Написать комментарий