Типы в TypeScript исчезают при компиляции, но в production мы сталкиваемся с данными из внешнего мира, где структурная типизация не способна защитить от путаницы между двумя сущностями одного базового типа — например, Email и UserId, объявленными как строки. Branded Types с Zod-схемами позволяют сохранить семантику в compile-time и runtime-валидацию для production-кода.
Проблема структурной типизации
Структурная типизация TypeScript не отличает Email от UserId, если оба — строки. Это приводит к ошибкам, когда в функцию отправки письма передаётся идентификатор пользователя. Branded Types добавляют невидимую метку на уровне типов, но не обеспечивают проверку в рантайме. Zod даёт реальную валидацию при работе с API, формами или env.
Решение с Zod и branded types
Используйте Zod для парсинга данных, извлекая типы через z.infer и добавляя brand-метку. Пример:
import { z } from 'zod';
const EmailSchema = z.string().email().brand('Email');
type Email = z.infer<typeof EmailSchema>;
function sendEmail(to: Email) { /* */ }
const email = EmailSchema.parse('user@example.com');
sendEmail(email); // compile-time + runtime pass
Типичная ошибка — попытка передать сырую строку напрямую в sendEmail. Branded type отсечёт это на этапе компиляции, а схема Zod перехватит некорректные данные из API ещё до логики.
Где применять и trade-offs
Подходит для моделей UserId, OrderId, SKU, чтобы избежать путаницы в API-клиентах, SDK или легаси-миграциях. Но:
- Не защищает от злонамеренной подделки — это не криптография, а дисциплина типов.
- Легко переборщить: для простых DTO достаточно обычных структурных типов.
- Без командных договорённостей бренды становятся кашей — кто-то использует Zod, кто-то самописные guards. Практический совет: ограничьте бренды только критичными сущностями на границе модулей.
Вывод: Branded Types с Zod решают проблему runtime-гарантий для доменных моделей за счёт дисциплины типов и zero-cost метки, но требуют строгих конвенций в команде.