← Курс/Conditional Types (условные типы)#172 из 257+35 XP

Conditional Types — условные типы

Синтаксис: `T extends U ? X : Y`

Conditional type работает как тернарный оператор, но на уровне типов: «если тип T совместим с типом U — используй X, иначе Y».

type IsString<T> = T extends string ? 'да, строка' : 'нет, не строка'

type A = IsString<string>   // 'да, строка'
type B = IsString<number>   // 'нет, не строка'
type C = IsString<'hello'>  // 'да, строка' — литеральный тип extends string

NonNullable — conditional type в стандартной библиотеке

NonNullable<T> убирает null и undefined из типа:

// Реализация в lib.d.ts:
type NonNullable<T> = T extends null | undefined ? never : T

type A = NonNullable<string | null>      // string
type B = NonNullable<number | undefined> // number
type C = NonNullable<null>               // never

Distributive conditional types

Когда T — union type, conditional type **распределяется** по каждому члену union:

type ToArray<T> = T extends any ? T[] : never

type A = ToArray<string | number>
// Распределяется как:
// ToArray<string> | ToArray<number>
// = string[] | number[]

// Чтобы отключить дистрибутивность — оберни в []
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type B = ToArrayNonDist<string | number>  // (string | number)[]

Ключевое слово `infer` — извлечение типов

infer позволяет «захватить» тип внутри extends и использовать его:

// ReturnType<T> — стандартный utility type, реализован через infer:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type A = ReturnType<() => string>         // string
type B = ReturnType<(x: number) => void>  // void
type C = ReturnType<typeof JSON.parse>    // any

// Извлечение типа элемента массива:
type ElementType<T> = T extends (infer E)[] ? E : never
type D = ElementType<string[]>   // string
type E = ElementType<number[][]> // number[]

// Извлечение типа Promise:
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T
// (рекурсивно разворачивает вложенные Promise)
type F = Awaited<Promise<string>>           // string
type G = Awaited<Promise<Promise<number>>>  // number

DeepReadonly через conditional types

type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonly<E>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T

interface Config {
  server: { host: string; port: number }
  flags: string[]
}

type ReadonlyConfig = DeepReadonly<Config>
// {
//   readonly server: { readonly host: string; readonly port: number }
//   readonly flags: ReadonlyArray<string>
// }

Практичные примеры conditional types

// Проверка — является ли тип функцией
type IsFunction<T> = T extends (...args: any[]) => any ? true : false

type A = IsFunction<() => void>  // true
type B = IsFunction<string>      // false

// Unpromisify — убирает обёртку Promise
type Unpromisify<T> = T extends Promise<infer R> ? R : T

// Parameters<T> — типы параметров функции
type Parameters<T> = T extends (...args: infer P) => any ? P : never

type Params = Parameters<(a: string, b: number) => void>
// [string, number]

// ConstructorParameters<T> — параметры конструктора
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never

Примеры

Runtime аналоги conditional types: deepFreeze, deepClone, deepMerge — условные преобразования объектов в JS

// В TypeScript conditional types работают на уровне системы типов.
// В JavaScript мы реализуем те же идеи как runtime-функции,
// которые условно преобразуют объекты в зависимости от их структуры.

// deepFreeze — рекурсивно замораживает объект (аналог DeepReadonly<T>)
function deepFreeze(obj) {
  if (obj === null || typeof obj !== 'object') return obj

  // Сначала замораживаем все вложенные объекты
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      deepFreeze(obj[key])
    }
  })

  return Object.freeze(obj)
}

// deepClone — глубокое копирование (аналог работы с immutable типами)
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj
  if (Array.isArray(obj)) return obj.map(deepClone)

  const cloned = {}
  Object.keys(obj).forEach(key => {
    cloned[key] = deepClone(obj[key])
  })
  return cloned
}

// deepMerge — слияние двух объектов (второй перекрывает первый)
function deepMerge(base, override) {
  if (typeof base !== 'object' || base === null) return override
  if (typeof override !== 'object' || override === null) return override

  const result = deepClone(base)

  Object.keys(override).forEach(key => {
    if (
      typeof override[key] === 'object' &&
      override[key] !== null &&
      typeof result[key] === 'object' &&
      result[key] !== null
    ) {
      result[key] = deepMerge(result[key], override[key])
    } else {
      result[key] = override[key]
    }
  })

  return result
}

// isFrozen — проверяет что объект и все вложенные заморожены
function isFrozen(obj) {
  if (obj === null || typeof obj !== 'object') return true
  if (!Object.isFrozen(obj)) return false
  return Object.keys(obj).every(key => isFrozen(obj[key]))
}

// --- Демонстрация ---

const config = {
  server: { host: 'localhost', port: 3000 },
  db: { host: 'localhost', port: 5432, credentials: { user: 'admin' } }
}

console.log('=== deepFreeze ===')
const frozenConfig = deepFreeze(deepClone(config))
console.log('Объект заморожен:', isFrozen(frozenConfig))           // true
console.log('Вложенный заморожен:', isFrozen(frozenConfig.server)) // true

// Попытка изменить замороженный объект молча проигнорируется
frozenConfig.server.port = 9999
console.log('port после попытки изменения:', frozenConfig.server.port) // 3000

console.log('\n=== deepClone ===')
const original = { a: 1, nested: { b: 2, arr: [1, 2, 3] } }
const clone = deepClone(original)
clone.nested.b = 999
clone.nested.arr.push(4)
console.log('Оригинал не изменился:', original.nested.b)         // 2
console.log('Клон изменён:', clone.nested.b)                     // 999
console.log('Массив оригинала:', original.nested.arr)            // [1, 2, 3]

console.log('\n=== deepMerge ===')
const defaults = {
  theme: 'light',
  settings: { lang: 'ru', timeout: 5000, debug: false }
}
const userPrefs = {
  theme: 'dark',
  settings: { lang: 'en', debug: true }
}
const merged = deepMerge(defaults, userPrefs)
console.log('theme:', merged.theme)              // 'dark'
console.log('lang:', merged.settings.lang)       // 'en'
console.log('timeout:', merged.settings.timeout) // 5000 (из defaults)
console.log('debug:', merged.settings.debug)     // true (из userPrefs)