Работа с union-типами
Работа с union-типами
Union-типы (A | B | C) — один из краеугольных камней системы типов TypeScript. Утилитарные типы этой группы позволяют фильтровать, извлекать и очищать union-типы.
Exclude<UnionType, ExcludedMembers>
Удаляет из union-типа все члены, которые совместимы с ExcludedMembers.
type Status = 'active' | 'inactive' | 'banned' | 'deleted'
// Убираем 'deleted' — в UI его не показываем
type VisibleStatus = Exclude<Status, 'deleted'>
// 'active' | 'inactive' | 'banned'
// Можно исключить несколько
type GoodStatus = Exclude<Status, 'banned' | 'deleted'>
// 'active' | 'inactive'
// Работает с любыми типами, не только с литералами
type Primitives = string | number | boolean | null | undefined
type Defined = Exclude<Primitives, null | undefined>
// string | number | boolean
type Exclude<T, U> = T extends U ? never : T
Это дистрибутивный условный тип: он применяется к каждому члену union отдельно. Для Exclude<‘a’ | ‘b’ | ‘c’, ‘a’>:
‘a’ extends ‘a’→never‘b’ extends ‘a’→‘b’‘c’ extends ‘a’→‘c’- Результат:
‘b’ | ‘c’
Extract<Type, Union>
Противоположность Exclude — извлекает из union только те члены, которые совместимы с Union.
type AllEvents =
| { type: 'click'; x: number; y: number }
| { type: 'keydown'; key: string }
| { type: 'scroll'; offset: number }
| { type: 'resize'; width: number; height: number }
// Извлекаем только события с координатами (x, y)
type PointEvent = Extract<AllEvents, { x: number }>
// { type: 'click'; x: number; y: number }
// Извлекаем из union только строковые литералы
type Mixed = 'hello' | 42 | true | 'world'
type OnlyStrings = Extract<Mixed, string>
// 'hello' | 'world'
// Практический пример: фильтрация событий по типу
type MouseEvents = Extract<AllEvents, { type: 'click' | 'scroll' }>
// { type: 'click'; x: number; y: number } | { type: 'scroll'; offset: number }
function handleMouseEvent(event: MouseEvents) {
if (event.type === 'click') {
console.log(`Клик в (${event.x}, ${event.y})`)
} else {
console.log(`Скролл: ${event.offset}px`)
}
}
type Extract<T, U> = T extends U ? T : never
Зеркальная копия Exclude: оставляет совпадающие, а не удаляет их.
NonNullable<T>
Удаляет null и undefined из union-типа. Это специализированная версия Exclude<T, null | undefined>.
type MaybeString = string | null | undefined
type DefinitelyString = NonNullable<MaybeString>
// string
// Типичный сценарий: обработка результатов DOM-запросов
function getElement(id: string): HTMLElement {
const el = document.getElementById(id)
// el имеет тип HTMLElement | null
if (!el) {
throw new Error(`Элемент #${id} не найден`)
}
// После проверки TypeScript сужает тип до HTMLElement
return el
}
// Или через NonNullable в типах
type Config = {
apiUrl: string | null
timeout: number | undefined
}
type RequiredConfig = {
[K in keyof Config]: NonNullable<Config[K]>
}
// { apiUrl: string; timeout: number }
// Начиная с TypeScript 4.8 — встроенная интринсика
// Концептуально эквивалентно:
type NonNullable<T> = T & {}
// или
type NonNullable<T> = Exclude<T, null | undefined>
Пересечение с {} (пустой объектный тип) убирает null и undefined, потому что они несовместимы с {}.
NoInfer<T>
Появился в TypeScript 5.4. Запрещает TypeScript использовать аргумент для вывода параметра типа. Позиция, обёрнутая в NoInfer, не участвует в автоматическом выводе — тип должен быть выведен из других аргументов.
// Без NoInfer: TypeScript выводит T из обоих аргументов
function createFSM<T extends string>(
initial: T,
states: T[]
) {}
// TypeScript объединяет оба аргумента для вывода T
// T = 'idle' | 'loading' | 'TYPO' — опечатка не обнаружена!
createFSM('idle', ['idle', 'loading', 'TYPO'])
// С NoInfer: T выводится только из states
function createFSM<T extends string>(
initial: NoInfer<T>,
states: T[]
) {}
// T = 'idle' | 'loading' | 'done' (выведено из states)
// initial проверяется на совместимость с T
createFSM('idle', ['idle', 'loading', 'done']) // OK
createFSM('TYPO', ['idle', 'loading', 'done']) // Ошибка!
// Ещё пример: значение по умолчанию в дженерике
function getOrDefault<T>(
value: T | undefined,
defaultValue: NoInfer<T>
): T {
return value ?? defaultValue
}
// T = number (выведено из первого аргумента)
getOrDefault(42, 0) // OK
getOrDefault(42, 'fallback') // Ошибка: string не совместим с number
NoInfer — это интринсик компилятора, а не тип, реализованный через дженерики. Он влияет только на алгоритм вывода типов и не изменяет результирующий тип.Практические паттерны
// Паттерн: безопасный диспетчер событий
type EventMap = {
click: { x: number; y: number }
keydown: { key: string; code: string }
scroll: { offset: number }
}
type EventName = keyof EventMap
// Exclude помогает создавать более узкие типы
type NonScrollEvents = Exclude<EventName, 'scroll'>
// 'click' | 'keydown'
// Extract помогает фильтровать union дискриминированных объединений
type StringEvents = Extract<EventName, `key${string}`>
// 'keydown'
// NonNullable — обязательный инструмент при работе с optional chaining
function getNestedValue(obj: { a?: { b?: string } }): string {
const value: string | undefined = obj.a?.b
// Приведение к NonNullable — если нужно гарантировать наличие
if (value == null) throw new Error('Значение отсутствует')
const result: NonNullable<typeof value> = value
return result
}