← Курс/Дистрибутивные Conditional Types#175 из 257+30 XP

Дистрибутивные Conditional Types

Как работает дистрибутивность

Когда вы применяете conditional type к generic-параметру, TypeScript автоматически «распределяет» его по каждому члену union-типа:

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

// С обычным типом:
type A = ToArray<string>  // string[]

// С union — распределяется автоматически!
type B = ToArray<string | number>
// = ToArray<string> | ToArray<number>
// = string[] | number[]

Это называется **дистрибутивностью** — conditional type распределяется по членам union.

Полезный паттерн: фильтрация union

// Оставляем только те типы, которые extends U
type Filter<T, U> = T extends U ? T : never

type Strings = Filter<string | number | boolean | null, string>
// = string (остальные → never, never объединяется с ничем)

type NonNullable<T> = T extends null | undefined ? never : T
type A = NonNullable<string | null | undefined>  // string

Отключение дистрибутивности через [T]

Оберните T в кортеж [T], чтобы предотвратить распределение:

// С дистрибутивностью:
type IsUnion<T> = T extends any ? [T] : never
type A = IsUnion<string | number>  // [string] | [number] — распределилось

// Без дистрибутивности — сравниваем union как целое:
type IsNeverDist<T>    = T extends never ? true : false
type IsNeverNoDist<T>  = [T] extends [never] ? true : false

type X = IsNeverDist<never>    // never (дистрибутивность по never даёт never!)
type Y = IsNeverNoDist<never>  // true  — правильно

Практические примеры дистрибутивных типов

// Убрать из union все функциональные типы
type NonFunction<T> = T extends (...args: any[]) => any ? never : T
type Clean = NonFunction<string | number | (() => void) | boolean>
// string | number | boolean

// Обернуть каждый тип в Promise
type Promisify<T> = T extends any ? Promise<T> : never
type P = Promisify<string | number>  // Promise<string> | Promise<number>

// Получить типы значений объекта
type ValueOf<T> = T extends Record<string, infer V> ? V : never

// Оba варианта выборки ключей
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never
}[keyof T]

interface Form {
  name: string
  age: number
  email: string
  active: boolean
}

type StringKeys = KeysOfType<Form, string>  // 'name' | 'email'

Рекурсивные conditional types

// Разворачиваем вложенные Promise
type DeepAwaited<T> = T extends Promise<infer R>
  ? DeepAwaited<R>
  : T

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

// Flatten — вытаскиваем тип из массива
type Flatten<T> = T extends Array<infer E> ? E : T
type B = Flatten<string[]>        // string
type C = Flatten<number[][][]>    // number[][] (один уровень)

// Рекурсивный Flatten:
type DeepFlatten<T> = T extends Array<infer E> ? DeepFlatten<E> : T
type D = DeepFlatten<number[][][]>  // number

Дистрибутивность в стандартной библиотеке

// Эти типы реализованы через дистрибутивные conditional types:
type NonNullable<T> = T extends null | undefined ? never : T
type Extract<T, U>  = T extends U ? T : never
type Exclude<T, U>  = T extends U ? never : T

Примеры

Runtime аналоги дистрибутивных типов: функции для фильтрации и трансформации наборов значений

// TypeScript conditional types работают на уровне типов.
// В JS реализуем те же идеи как функции над массивами значений.

// Аналог: type Filter<T, U> = T extends U ? T : never
// Runtime-версия: фильтрация по предикату типа
function filterByType(values, typePredicate) {
  return values.filter(typePredicate)
}

// Аналог: type NonNullable<T> = T excludes null | undefined
function removeNullable(values) {
  return values.filter(v => v != null)
}

// Аналог: type Promisify<T> — оборачиваем значения
function promisifyAll(values) {
  return values.map(v => Promise.resolve(v))
}

// Аналог: type Extract<T, U> = T extends U ? T : never
// Оставляем только значения определённого типа
function extract(values, type) {
  return values.filter(v => typeof v === type || (type === 'array' && Array.isArray(v)))
}

// Аналог: type Exclude<T, U> = T extends U ? never : T
function exclude(values, type) {
  return values.filter(v => typeof v !== type)
}

// Аналог DeepFlatten — рекурсивно разворачиваем массивы
function deepFlatten(arr) {
  return arr.reduce((acc, item) => {
    if (Array.isArray(item)) {
      return acc.concat(deepFlatten(item))
    }
    return acc.concat(item)
  }, [])
}

// Аналог Awaited — ждём все Promise в union
async function resolveAll(values) {
  return Promise.all(values.map(v =>
    v instanceof Promise ? v : Promise.resolve(v)
  ))
}

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

const mixed = [1, 'hello', null, true, undefined, 42, 'world', null, false]

console.log('=== NonNullable ===')
const noNulls = removeNullable(mixed)
console.log('Без null/undefined:', noNulls)

console.log('\n=== Extract (только строки) ===')
const strings = extract(mixed, 'string')
console.log('Строки:', strings)

console.log('\n=== Exclude (убрать числа) ===')
const noNumbers = exclude(mixed, 'number')
console.log('Без чисел:', noNumbers)

console.log('\n=== DeepFlatten ===')
const nested = [1, [2, 3], [4, [5, [6, 7]]]]
console.log('Вложенный:', JSON.stringify(nested))
console.log('После flatten:', deepFlatten(nested))

console.log('\n=== Union фильтрация (filterByType) ===')
const types = [
  { type: 'admin',    name: 'Алексей' },
  { type: 'user',     name: 'Ольга'   },
  { type: 'admin',    name: 'Иван'    },
  { type: 'moderator',name: 'Мария'   },
]

const admins = filterByType(types, t => t.type === 'admin')
console.log('Администраторы:', admins.map(t => t.name))

console.log('\n=== Awaited (Promise union) ===')
const maybePromises = [
  Promise.resolve(1),
  42,
  Promise.resolve('hello'),
  'world',
]

resolveAll(maybePromises).then(results => {
  console.log('Resolved:', results)
})