Discriminated Unions (tagged union)
Идиома №1 для моделирования состояний (loading/success/error, события, ADT). На интервью обязательно спросят про exhaustiveness check через never. Красный флаг — забывать дефолт-ветку, делать не-литеральный дискриминатор, использовать boolean там, где нужен ‘success’ | ‘error’.
Состояние UI как DU:
type State<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error };
function render<T>(s: State<T>): string { switch (s.status) { case 'idle': return '—'; case 'loading': return '...'; case 'success': return JSON.stringify(s.data); case 'error': return s.error.message; default: { // exhaustiveness check const _never: never = s; return _never; } }}Без exhaustiveness check добавление нового варианта молча сломает логику:
type Event = | { type: 'click'; x: number; y: number } | { type: 'key'; key: string };
function handle(e: Event) { switch (e.type) { case 'click': return `${e.x},${e.y}`; case 'key': return e.key; // Добавим новый вариант 'wheel' — компилятор подсветит default ниже }}Хелпер assertNever и pattern matching через satisfies:
function assertNever(x: never): never { throw new Error('unhandled: ' + JSON.stringify(x));}
type Shape = | { kind: 'circle'; r: number } | { kind: 'square'; size: number };
const area = (s: Shape) => { switch (s.kind) { case 'circle': return Math.PI * s.r ** 2; case 'square': return s.size ** 2; default: return assertNever(s); }};Итог: DU + literal discriminator + assertNever = безопасные конечные автоматы и события без забытых веток.