Feature flags в production — удобно, но конфиги часто превращаются в мешанину строк. Простое
{ isNewFeature: true } не защищает от опечаток вроде isNewFeautre, и линтер молчит до прогона тестов или деплоя. Используем branded types с Zod для строгой проверки на этапе компиляции и выполнения.Branded types для compile-time безопасности
Тип
Brand<T, B> добавляет уникальную метку. Передать простой boolean вместо NewFeature не выйдет — TS потребует явного приведения. Это исключает путаницу между флагами:type Brand<T, B> = T & { __brand: B };
type NewFeature = Brand<boolean, 'NewFeature'>;
type LegacyFallback = Brand<boolean, 'LegacyFallback'>;
function useFeature(newFeature: NewFeature, legacyFallback: LegacyFallback): boolean {
return newFeature || legacyFallback;
}Runtime validation с Zod
Даже с branded types в JSON может прилететь невалидное значение. Добавляем схему с
z.boolean().brand() — тогда при парсинге ошибка будет явной, а не тихим undefined:const ConfigSchema = z.object({
newFeature: z.boolean().brand<'NewFeature'>(),
legacyFallback: z.boolean().brand<'LegacyFallback'>(),
});
type Config = z.infer<typeof ConfigSchema>;
function loadConfig(raw: unknown): Config {
return ConfigSchema.parse(raw);
}Дискриминируемые union для состояний флагов
Когда два флага конфликтуют (например,
newFeature и legacyFallback), обычные типы не мешают включить оба одновременно. Discriminated union сужает область до корректных комбинаций — это предотвращает два источника истины:type FlagState =
| { newFeature: true; legacyFallback: false }
| { newFeature: false; legacyFallback: boolean };
function processState(state: FlagState) {
if (state.newFeature) {
return state.legacyFallback; // TS сужает до false
}
return state.legacyFallback;
}
* Предупреждение: branded types добавляют шум в код и усложняют вложенные конфиги без линтера. Для плоских A/B тестов и канареечных релизов это окупается — ошибка ловится на этапе компиляции, а не в production.
* Совет: комбинируй discriminated union с zod-схемой на весь конфиг — тогда даже опечатка в JSON упадёт с понятным сообщением, а не молча сломает логику.
Вывод: Типобезопасная композиция флагов через branded types и runtime-валидация даёт двухуровневую защиту — compile-time строгость и runtime-проверку входных данных, что критично для надёжности A/B экспериментов и канареечных деплоев.