Skip to content

Instantly share code, notes, and snippets.

@olegopro
Last active May 9, 2025 19:46
Show Gist options
  • Save olegopro/d6a39a922af8d619298878f6a714d4e6 to your computer and use it in GitHub Desktop.
Save olegopro/d6a39a922af8d619298878f6a714d4e6 to your computer and use it in GitHub Desktop.

Детальный анализ сложного типа в 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?

Для новичков в TypeScript не всегда очевидно, почему результатом выражения с маппированным типом и индексным доступом

{
  [K in keyof UserData]: ExtractPrimitives<UserData[K]>
}[keyof UserData]

становится number | string | boolean, а тип never (соответствующий полю metadata) отфильтровывается.

Это происходит благодаря двум механизмам TypeScript:

  1. Индексный доступ с union-типом ключей: Сначала маппированный тип создаёт новый объектный тип:

    {
      id: number,
      name: string,
      registered: boolean,
      metadata: never   // object отфильтрован в never
    }
  2. Применение индексного доступа [keyof UserData]: Когда применяется индексный доступ с использованием union-типа ключей (а keyof UserData это union "id" | "name" | "registered" | "metadata"), TypeScript возвращает union всех возможных типов значений по этим ключам:

    {
      id: number,
      name: string,
      registered: boolean,
      metadata: never
    }["id" | "name" | "registered" | "metadata"]
  3. Формирование union всех типов значений:

    number | string | boolean | never
  4. Особое поведение типа never: Ключевой момент: в TypeScript тип never в union-типах автоматически исчезает, поскольку он представляет "пустое множество значений". По определению, ни одно значение не может иметь тип never, поэтому при объединении с другими типами он поглощается.

    Это происходит потому что:

    • По определению, тип never не может содержать никаких значений
    • Объединение с пустым множеством не добавляет новых значений
  5. Финальный результат:

    number | string | boolean

Такой механизм позволяет эффективно "отфильтровывать" нежелательные типы из union-типов на этапе компиляции. Фактически, конструкция [keyof T] в конце выражения часто используется именно для того, чтобы "развернуть" объект в union его значений, отбросив при этом все свойства с типом never.

Подробное объяснение маппированного типа

Запись [K in keyof UserData]: ExtractPrimitives<UserData[K]> - это пример маппированного типа в TypeScript, который многим разработчикам JavaScript кажется странным, так как в JavaScript нет подобного синтаксиса. Разберем пошагово, как работает эта запись:

  1. keyof UserData - это выражение, которое возвращает объединение (union) всех ключей объекта UserData: "id" | "name" | "registered" | "metadata"
  2. [K in keyof UserData] - это начало создания маппированного типа. Можно представить это как цикл, который проходит по каждому ключу из объекта UserData. K - это переменная цикла, которая принимает значение каждого ключа по очереди.
  3. После двоеточия ExtractPrimitives<UserData[K]> - это то, что будет присвоено каждому ключу в новом типе. Для каждого ключа K выполняется:
    • UserData[K] - получение типа значения по ключу K (например, для K = "id" это будет "number")
    • ExtractPrimitives<...> - применение условного типа к этому значению

В результате для каждого ключа исходного объекта происходит проверка: если тип значения является примитивом, он сохраняется, иначе становится never.


Этот тип позволяет извлечь все примитивные типы из объекта UserData, отфильтровав непримитивные типы, такие как object. Рассмотрим этот механизм шаг за шагом.

Исходные типы

Для начала, разберем базовые компоненты нашего типа:

  1. Primitive - объединение (union) примитивных типов в TypeScript:

    type Primitive = string | number | boolean | null | undefined
  2. ExtractPrimitives - условный тип (conditional type), который работает как фильтр:

    type ExtractPrimitives<T> = T extends Primitive ? T : never

    Он проверяет: если T является подтипом Primitive, то возвращает T, иначе возвращает never.

  3. UserData - обычный объектный тип с различными свойствами:

    type UserData = {
      id: number
      name: string
      registered: boolean
      metadata: object
    };

    Здесь свойства id, name и registered имеют примитивные типы, а metadata - непримитивный тип object.

Маппированный тип: первый этап разбора

Теперь разберем первую часть нашего сложного типа:

{ [K in keyof UserData]: ExtractPrimitives<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

Таким образом, успешно извлечены все примитивные типы из объекта UserData и отфильтрованы непримитивные.

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

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

  1. Фильтрация типов - извлечение подмножества типов из сложных структур данных по определенному критерию

  2. Преобразование данных - создание новых структур типов на основе существующих

  3. Валидация типов - проверка соответствия данных определенным ограничениям на этапе компиляции

  4. Создание утилитарных типов - разработка переиспользуемых утилит для работы с типами

  5. Интеграция с API - преобразование типов данных, полученных из внешних источников, в более удобный формат

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

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment