Discriminated Union (теговый union, tagged union) — это паттерн, где несколько типов объединяются через общее **дискриминирующее поле** (обычно называемое kind, type, tag), которое содержит уникальный литеральный тип для каждого варианта.
// Без discriminated union — сложно различить варианты:
interface CircleOrRect {
radius?: number // только для круга
width?: number // только для прямоугольника
height?: number // только для прямоугольника
}
// Как понять что перед нами?
// С discriminated union — чисто и безопасно:
interface Circle {
kind: 'circle' // дискриминирующее поле
radius: number
}
interface Rectangle {
kind: 'rectangle' // дискриминирующее поле
width: number
height: number
}
type Shape = Circle | RectangleTypeScript автоматически сужает тип в ветках switch или if по дискриминирующему полю:
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// Здесь TypeScript знает: shape: Circle
return Math.PI * shape.radius ** 2
case 'rectangle':
// Здесь TypeScript знает: shape: Rectangle
return shape.width * shape.height
}
}
// Работает и с if:
if (shape.kind === 'circle') {
shape.radius // OK — TypeScript знает что это Circle
}Добавление default ветки с never гарантирует что при добавлении нового типа в union TypeScript выдаст ошибку:
type Shape = Circle | Rectangle | Triangle
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2
case 'rectangle': return shape.width * shape.height
// Если забыть 'triangle' — TypeScript выдаст ошибку:
default:
const exhaustiveCheck: never = shape
// Ошибка: Type 'Triangle' is not assignable to type 'never'
throw new Error(`Неизвестная фигура: ${(exhaustiveCheck as any).kind}`)
}
}Discriminated unions идеальны для состояний в UI:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
function renderState(state: RequestState<User[]>): string {
switch (state.status) {
case 'idle': return 'Нажмите для загрузки'
case 'loading': return 'Загрузка...'
case 'success': return `Найдено ${state.data.length} пользователей`
case 'error': return `Ошибка: ${state.error}`
}
}Discriminated unions используются в Redux-подобных паттернах:
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' }
| { type: 'SET_VALUE'; payload: number }
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
case 'RESET': return 0
case 'SET_VALUE': return action.payload // TypeScript знает: action.payload существует
}
}1. **Безопасный доступ к полям** — TypeScript не даст обратиться к radius у прямоугольника
2. **Exhaustive checks** — TS предупредит если не обработали все варианты
3. **Читаемость** — поле kind/type явно документирует вариант
4. **Рефакторинг** — при добавлении нового варианта TypeScript найдёт все места для обновления
Discriminated unions: паттерн с kind-полем и исчерпывающие проверки
// В TS: discriminated union с полем kind/type
// В JS: тот же паттерн, но без compile-time проверок
// === Фигуры с discriminated union ===
function getArea(shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'rectangle':
return shape.width * shape.height
case 'triangle':
return (shape.base * shape.height) / 2
default:
// В TS: const check: never = shape — ошибка если добавить новый тип
throw new Error(`Неизвестная фигура: ${shape.kind}`)
}
}
function describeShape(shape) {
switch (shape.kind) {
case 'circle': return `Круг с радиусом ${shape.radius}`
case 'rectangle': return `Прямоугольник ${shape.width}×${shape.height}`
case 'triangle': return `Треугольник: основание ${shape.base}, высота ${shape.height}`
default:
throw new Error(`Неизвестная фигура: ${shape.kind}`)
}
}
const shapes = [
{ kind: 'circle', radius: 5 },
{ kind: 'rectangle', width: 4, height: 6 },
{ kind: 'triangle', base: 3, height: 8 },
]
console.log('=== Фигуры ===')
shapes.forEach(shape => {
console.log(`${describeShape(shape)} — площадь: ${getArea(shape).toFixed(2)}`)
})
// === Паттерн: State Machine для загрузки данных ===
console.log('\n=== Request State Machine ===')
function renderState(state) {
// В TS: state: RequestState<User[]>
// TypeScript через discriminated union знает какие поля доступны в каждом case
switch (state.status) {
case 'idle': return 'Нажмите для загрузки'
case 'loading': return 'Загрузка данных...'
case 'success': return `Загружено ${state.data.length} записей: ${state.data.join(', ')}`
case 'error': return `Ошибка: ${state.error}`
default:
throw new Error(`Неизвестный статус: ${state.status}`)
}
}
const states = [
{ status: 'idle' },
{ status: 'loading' },
{ status: 'success', data: ['Алексей', 'Мария', 'Дмитрий'] },
{ status: 'error', error: 'Сервер недоступен' },
]
states.forEach(state => {
console.log(renderState(state))
})
// === Redux-like reducer с discriminated union ===
console.log('\n=== Reducer (Action pattern) ===')
function counterReducer(state, action) {
// В TS: action: Action — discriminated union по полю type
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
case 'RESET': return 0
case 'SET_VALUE': return action.payload // только у SET_VALUE есть payload
default:
throw new Error(`Неизвестный action: ${action.type}`)
}
}
let count = 0
const actions = [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'DECREMENT' },
{ type: 'SET_VALUE', payload: 10 },
{ type: 'INCREMENT' },
]
actions.forEach(action => {
count = counterReducer(count, action)
console.log(`${action.type}: ${count}`)
})Discriminated Union (теговый union, tagged union) — это паттерн, где несколько типов объединяются через общее **дискриминирующее поле** (обычно называемое kind, type, tag), которое содержит уникальный литеральный тип для каждого варианта.
// Без discriminated union — сложно различить варианты:
interface CircleOrRect {
radius?: number // только для круга
width?: number // только для прямоугольника
height?: number // только для прямоугольника
}
// Как понять что перед нами?
// С discriminated union — чисто и безопасно:
interface Circle {
kind: 'circle' // дискриминирующее поле
radius: number
}
interface Rectangle {
kind: 'rectangle' // дискриминирующее поле
width: number
height: number
}
type Shape = Circle | RectangleTypeScript автоматически сужает тип в ветках switch или if по дискриминирующему полю:
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// Здесь TypeScript знает: shape: Circle
return Math.PI * shape.radius ** 2
case 'rectangle':
// Здесь TypeScript знает: shape: Rectangle
return shape.width * shape.height
}
}
// Работает и с if:
if (shape.kind === 'circle') {
shape.radius // OK — TypeScript знает что это Circle
}Добавление default ветки с never гарантирует что при добавлении нового типа в union TypeScript выдаст ошибку:
type Shape = Circle | Rectangle | Triangle
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2
case 'rectangle': return shape.width * shape.height
// Если забыть 'triangle' — TypeScript выдаст ошибку:
default:
const exhaustiveCheck: never = shape
// Ошибка: Type 'Triangle' is not assignable to type 'never'
throw new Error(`Неизвестная фигура: ${(exhaustiveCheck as any).kind}`)
}
}Discriminated unions идеальны для состояний в UI:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string }
function renderState(state: RequestState<User[]>): string {
switch (state.status) {
case 'idle': return 'Нажмите для загрузки'
case 'loading': return 'Загрузка...'
case 'success': return `Найдено ${state.data.length} пользователей`
case 'error': return `Ошибка: ${state.error}`
}
}Discriminated unions используются в Redux-подобных паттернах:
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' }
| { type: 'SET_VALUE'; payload: number }
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
case 'RESET': return 0
case 'SET_VALUE': return action.payload // TypeScript знает: action.payload существует
}
}1. **Безопасный доступ к полям** — TypeScript не даст обратиться к radius у прямоугольника
2. **Exhaustive checks** — TS предупредит если не обработали все варианты
3. **Читаемость** — поле kind/type явно документирует вариант
4. **Рефакторинг** — при добавлении нового варианта TypeScript найдёт все места для обновления
Discriminated unions: паттерн с kind-полем и исчерпывающие проверки
// В TS: discriminated union с полем kind/type
// В JS: тот же паттерн, но без compile-time проверок
// === Фигуры с discriminated union ===
function getArea(shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2
case 'rectangle':
return shape.width * shape.height
case 'triangle':
return (shape.base * shape.height) / 2
default:
// В TS: const check: never = shape — ошибка если добавить новый тип
throw new Error(`Неизвестная фигура: ${shape.kind}`)
}
}
function describeShape(shape) {
switch (shape.kind) {
case 'circle': return `Круг с радиусом ${shape.radius}`
case 'rectangle': return `Прямоугольник ${shape.width}×${shape.height}`
case 'triangle': return `Треугольник: основание ${shape.base}, высота ${shape.height}`
default:
throw new Error(`Неизвестная фигура: ${shape.kind}`)
}
}
const shapes = [
{ kind: 'circle', radius: 5 },
{ kind: 'rectangle', width: 4, height: 6 },
{ kind: 'triangle', base: 3, height: 8 },
]
console.log('=== Фигуры ===')
shapes.forEach(shape => {
console.log(`${describeShape(shape)} — площадь: ${getArea(shape).toFixed(2)}`)
})
// === Паттерн: State Machine для загрузки данных ===
console.log('\n=== Request State Machine ===')
function renderState(state) {
// В TS: state: RequestState<User[]>
// TypeScript через discriminated union знает какие поля доступны в каждом case
switch (state.status) {
case 'idle': return 'Нажмите для загрузки'
case 'loading': return 'Загрузка данных...'
case 'success': return `Загружено ${state.data.length} записей: ${state.data.join(', ')}`
case 'error': return `Ошибка: ${state.error}`
default:
throw new Error(`Неизвестный статус: ${state.status}`)
}
}
const states = [
{ status: 'idle' },
{ status: 'loading' },
{ status: 'success', data: ['Алексей', 'Мария', 'Дмитрий'] },
{ status: 'error', error: 'Сервер недоступен' },
]
states.forEach(state => {
console.log(renderState(state))
})
// === Redux-like reducer с discriminated union ===
console.log('\n=== Reducer (Action pattern) ===')
function counterReducer(state, action) {
// В TS: action: Action — discriminated union по полю type
switch (action.type) {
case 'INCREMENT': return state + 1
case 'DECREMENT': return state - 1
case 'RESET': return 0
case 'SET_VALUE': return action.payload // только у SET_VALUE есть payload
default:
throw new Error(`Неизвестный action: ${action.type}`)
}
}
let count = 0
const actions = [
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'INCREMENT' },
{ type: 'DECREMENT' },
{ type: 'SET_VALUE', payload: 10 },
{ type: 'INCREMENT' },
]
actions.forEach(action => {
count = counterReducer(count, action)
console.log(`${action.type}: ${count}`)
})Реализуй функцию `processNotification(notification)`, которая обрабатывает уведомления разных типов. Типы: { kind: "email", to: string, subject: string }, { kind: "sms", phone: string, text: string }, { kind: "push", title: string, body: string }. Функция должна возвращать строку описания для каждого типа. Для неизвестного kind бросать ошибку.
Используй switch (notification.kind) с тремя case: "email", "sms", "push". В каждом case обращайся к нужным полям объекта. В default: throw new Error(`Неизвестный тип: ${notification.kind}`). В TypeScript компилятор не даст обратиться к notification.to если kind !== "email".
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке