ПрограммированиеTypeScript architect

Как реализуется сопоставление с образцом (pattern matching) в TypeScript через discriminated unions? Как правильно структурировать типы и какие ловушки бывают?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В TypeScript сопоставление с образцом реализуется через "discriminated unions" (объединения по дискриминанту). Каждому объекту в объединении назначается обязательное поле-дискриминатор (обычно строка, например type), по которому TypeScript различает варианты.

Пример:

type Success = { type: 'success'; data: string }; type Failure = { type: 'failure'; error: string }; type Result = Success | Failure; function handleResult(result: Result) { switch (result.type) { case 'success': // result: Success console.log(result.data); break; case 'failure': // result: Failure console.error(result.error); break; } }

В switch/case или if по полю-дискриминатору TypeScript "сузит" тип точно до нужного варианта.

Главные достоинства:

  • Строгая типизация — нельзя обратиться к несуществующему полю.
  • Проверка exhaustiveness — если не обработать все варианты, иногда срабатывает ошибка (можно явно форсировать).

Вопрос с подвохом

Если добавить новый вариант в discriminated union, будет ли TypeScript принудительно требовать обновить все switch-case, чтобы обрабатывался новый вариант?

Ответ: Нет, только если явно добавить обработку "невозможного" варианта. Например, использовать функцию never:

Пример:

function assertNever(x: never): never { throw new Error('Unexpected variant: ' + x); } function handle(r: Result) { switch(r.type) { case 'success': /* ... */; break; case 'failure': /* ... */; break; default: return assertNever(r); // TS выдаст ошибку, если появится новый тип } }

Примеры реальных ошибок из-за незнания тонкостей темы.


История

После расширения типа "Result" новым вариантом ('pending') в нескольких местах приложения старые switch-case не обработали этот кейс. В результате часть интерфейсов перестала работать. Ошибка была замечена только в production спустя неделю после релиза.


История

Попытка использовать discriminated union без уникального дискриминатора (type поле дублировалось в двух типах) привела к "размытию" типов: TypeScript перестал точно сужать тип, и стало возможным обращение к несуществующим полям без ошибки компиляции. Несколько критичных багов ушли в прод.


История

В проекте pattern matching реализовали через if-else по нескольким полям вместо использования одного явного дискриминатора. Это усложнило переход на exhaustiveness-проверку с never-функцией и усложнило читаемость кода — switch-case работали некорректно, и новые варианты "ломали" существующую логику.