← Курс/infer: извлечение типов в Conditional Types#156 из 257+35 XP

infer: извлечение типов в Conditional Types

Что такое infer

infer — ключевое слово, которое работает только внутри **conditional types** (условных типов). Оно позволяет TypeScript **вывести** и **захватить** часть типа во время сопоставления с образцом, чтобы использовать её в результате.

// Синтаксис:
type MyType<T> = T extends SomeType<infer U> ? U : never
//                                   ^^^^^^
//                   infer захватывает часть типа в переменную U

ReturnType: извлечение типа возврата функции

Самый известный пример использования infer — встроенный ReturnType<T>:

// Как устроен ReturnType изнутри:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
//                                                ^^^^^^
//                               infer R захватывает тип возвращаемого значения

function createUser(name: string, age: number) {
  return { id: Math.random(), name, age, createdAt: new Date() }
}

type User = ReturnType<typeof createUser>
// type User = { id: number; name: string; age: number; createdAt: Date }

Parameters: извлечение параметров функции

// Как устроен Parameters изнутри:
type Parameters<T> = T extends (...args: infer P) => any ? P : never

function sendEmail(to: string, subject: string, body: string): Promise<void> {
  return fetch('/api/email', { method: 'POST', body: JSON.stringify({ to, subject, body }) })
    .then(() => {})
}

type EmailParams = Parameters<typeof sendEmail>
// type EmailParams = [to: string, subject: string, body: string]

Awaited: разворачивание Promise

// Встроенный Awaited<T> (упрощённо):
type Awaited<T> = T extends Promise<infer U> ? U : T

type Result = Awaited<Promise<string>>   // type Result = string
type Result2 = Awaited<Promise<User[]>>  // type Result2 = User[]
type Result3 = Awaited<number>           // type Result3 = number (не Promise — возвращает как есть)

// Для вложенных Promise:
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T

type R = DeepAwaited<Promise<Promise<string>>>  // string

ElementType: тип элемента массива

type ElementType<T> = T extends Array<infer U> ? U : never

type Nums = ElementType<number[]>    // type Nums = number
type Strs = ElementType<string[]>    // type Strs = string
type Never = ElementType<string>     // type Never = never (не массив)

Практические примеры

Первый элемент кортежа:

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never

type H1 = Head<[string, number, boolean]>  // string
type H2 = Head<[number]>                    // number
type H3 = Head<[]>                          // never

Последний элемент кортежа:

type Last<T extends any[]> = T extends [...any[], infer L] ? L : never

type L1 = Last<[string, number, boolean]>   // boolean
type L2 = Last<[string]>                     // string

Извлечение типа из Generic:

type UnpackPromise<T> = T extends Promise<infer U> ? U : T
type UnpackArray<T> = T extends Array<infer U> ? U : T

// Можно комбинировать:
type Unpack<T> = T extends Promise<infer U>
  ? Unpack<U>
  : T extends Array<infer V>
    ? V
    : T

type A = Unpack<Promise<string[]>>   // string

Ограничения infer

  • infer работает только внутри extends в conditional types
  • Нельзя использовать infer в обычных типах
  • Переменная, захваченная infer, существует только в ветке true условного типа
  • Примеры

    Паттерны infer в JavaScript: извлечение типов возврата и параметров функций через метапрограммирование

    // В TS: infer извлекает типы на уровне типов (compile-time)
    // В JS: показываем runtime-аналоги — извлечение информации о функциях
    
    // === ReturnType паттерн: фабрика с отложенным созданием ===
    // В TS: ReturnType<typeof createUser> автоматически даёт тип
    // В JS: мы знаем что вернёт функция из её вызова
    
    function createUser(name, age) {
      return {
        id: Math.floor(Math.random() * 1000),
        name,
        age,
        createdAt: new Date().toISOString(),
      }
    }
    
    function createProduct(name, price, category) {
      return {
        id: Math.floor(Math.random() * 1000),
        name,
        price,
        category,
        inStock: true,
      }
    }
    
    // "ReturnType" в JS — просто создаём экземпляр и смотрим на ключи:
    const userShape = createUser('', 0)
    const productShape = createProduct('', 0, '')
    
    console.log('=== Структура User (как ReturnType) ===')
    console.log('Ключи User:', Object.keys(userShape))
    // ['id', 'name', 'age', 'createdAt']
    
    console.log('Ключи Product:', Object.keys(productShape))
    // ['id', 'name', 'price', 'category', 'inStock']
    
    // === Awaited паттерн: разворачивание Promise ===
    console.log('\n=== Awaited паттерн ===')
    
    async function fetchUsers() {
      // В TS: ReturnType<typeof fetchUsers> = Promise<User[]>
      // Awaited<ReturnType<typeof fetchUsers>> = User[]
      return [
        { id: 1, name: 'Алексей' },
        { id: 2, name: 'Мария' },
      ]
    }
    
    async function fetchWithRetry(fn, retries) {
      for (let i = 0; i < retries; i++) {
        try {
          // Разворачиваем Promise (как Awaited<T>)
          const result = await fn()
          return result
        } catch (e) {
          if (i === retries - 1) throw e
          console.log(`Попытка ${i + 1} не удалась, повтор...`)
        }
      }
    }
    
    // Демонстрация async/await как Awaited<Promise<T>>
    fetchUsers().then(users => {
      console.log('Пользователи:', users.length)  // 2
      console.log('Тип элемента (как ElementType):', typeof users[0])  // object
    })
    
    // === ElementType паттерн: тип элемента массива ===
    console.log('\n=== ElementType паттерн ===')
    
    function getFirstElement(arr) {
      // В TS: function getFirst<T>(arr: T[]): T | undefined
      // infer захватил бы T из T[]
      if (arr.length === 0) return undefined
      return arr[0]
    }
    
    function mapElements(arr, transform) {
      // В TS: <T, U>(arr: T[], transform: (item: T) => U): U[]
      return arr.map(transform)
    }
    
    const numbers = [1, 2, 3, 4, 5]
    const strings = ['hello', 'world', 'typescript']
    
    console.log(getFirstElement(numbers))  // 1
    console.log(getFirstElement(strings))  // 'hello'
    
    const doubled = mapElements(numbers, x => x * 2)
    const upper = mapElements(strings, s => s.toUpperCase())
    
    console.log('Doubled:', doubled)  // [2, 4, 6, 8, 10]
    console.log('Upper:', upper)      // ['HELLO', 'WORLD', 'TYPESCRIPT']
    
    // === Parameters паттерн: мемоизация с автовыводом типов ===
    console.log('\n=== Parameters паттерн (мемоизация) ===')
    
    function memoize(fn) {
      // В TS: <T extends (...args: any[]) => any>(fn: T): T
      // Parameters<T> — тип аргументов, ReturnType<T> — тип результата
      const cache = new Map()
      return function(...args) {
        const key = JSON.stringify(args)
        if (cache.has(key)) {
          console.log('  [из кэша]')
          return cache.get(key)
        }
        const result = fn(...args)
        cache.set(key, result)
        return result
      }
    }
    
    const expensiveCalc = memoize((n) => {
      console.log('  [вычисление]')
      return n * n
    })
    
    console.log(expensiveCalc(5))   // [вычисление] 25
    console.log(expensiveCalc(5))   // [из кэша] 25
    console.log(expensiveCalc(10))  // [вычисление] 100