Feature flags type-safe em TypeScript: configuração, generics e type guards
Por John Pires · 03 de abril de 2026

Feature flags type-safe em TypeScript: configuração, generics e type guards
O que é e por que importa
Feature flags (ou feature toggles) são chaves que ligam ou desligam funcionalidades em tempo de execução. Em vez de deploys separados para cada mudança, você controla o comportamento do sistema por configuração. Para quem está começando, pense em um painel de controle: cada botão decide se um recurso aparece para o usuário.
Em TypeScript, o desafio é manter essa flexibilidade sem perder a segurança de tipos. Se a lista de flags for apenas strings soltas, você perde autocompletar, checagem em tempo de compilação e corre risco de erros sutis (typos, flags inexistentes). A solução é modelar as flags com tipos - type-safe - para que o compilador te impeça de usar flags que não existem.
Neste artigo você aprenderá do zero como estruturar uma configuração de feature flags type-safe usando generics, utility types e type guards. As abordagens seguem uma progressão do mais simples para um exemplo prático completo. Vou guiar passo a passo com analogias e exemplos que compilam em TypeScript.
Por que investir em type-safety?
Em equipes pequenas ou em código legado, erros com nomes de flag causam bugs difíceis de reproduzir. Type-safety reduz tempo gasto em debugging e aumenta confiança ao refatorar. Além disso, permite autocompletar nos editores, documentação implícita e contratos claros entre módulos.
Mesmo em projetos simples, definir um contrato tipado para flags é uma boa prática: evita cheiros de código e facilita a evolução do produto.
Analogias rápidas
Pense em flags como interruptores numerados em uma central elétrica. Se você rotular mal um interruptor, vai ligar a luz errada. Tipos são as etiquetas: garantem que você sempre encontre o interruptor certo sem abrir o painel.
Conceitos fundamentais com analogias
Antes de codificar, precisamos entender alguns conceitos de TypeScript aplicados às flags: tipos literais, mapeamento de tipos, Generics e Type Guards. Vou explicar cada um com analogias simples e depois mostrar como se aplicam.
Tipos literais e mapeamentos
Tipos literais em TypeScript permitem limitar valores possíveis a um conjunto específico, por exemplo: 'login' | 'payments'. Mapear um objeto a partir desses literais cria um contrato entre a configuração e o código que consome as flags.
Analogia: imagine uma planilha com colunas. Cada coluna (flag) tem um cabeçalho (nome da flag) e um valor (true/false). O tipo garante que você só consulte cabeçalhos existentes.
Generics e utility types
Generics permitem escrever funções e tipos que trabalham com qualquer conjunto de flags sem perder a informação específica de cada projeto. Utility types (Record, Partial, Pick) ajudam a transformar e validar formatos. Analogia: generics são moldes, você coloca o material (o conjunto de flags) e obtém peças que combinam exatamente.
Type guards
Type guards são funções que, em tempo de execução, afirmam para o compilador qual tipo algo tem. Eles são essenciais quando você recebe dados dinâmicos (por exemplo, JSON de um backend) e quer converter isso para seu tipo de flags sem perder segurança.
Analogia: o guard é como um fiscal que checa a identidade de um objeto e, depois de conferir, permite que você trate esse objeto com tipos fortes.
Passo a passo com código comentado
Aqui começa a parte prática. Vamos criar uma base mínima, validável e ampliável de feature flags type-safe. Cada bloco tem contexto antes e explicação depois.
1) Definindo o conjunto de flags com tipos literais
Vamos declarar as flags do sistema como um tipo literal e um objeto de configuração inicial.
O que este bloco faz: define um tipo enumerado de flags e uma configuração padrão.
// flags.ts
export const FEATURE_KEYS = ['login', 'payments', 'betaDashboard'] as const;
export type FeatureKey = typeof FEATURE_KEYS[number];
export type FeatureConfig = Record<FeatureKey, boolean>;
export const defaultFeatures: FeatureConfig = {
login: true,
payments: false,
betaDashboard: false,
};
O que acontece: FEATURE_KEYS é um tuple readonly; FeatureKey é um tipo literal 'login'|'payments'|'betaDashboard'. FeatureConfig é um Record que garante que todo key exista e tenha boolean.
2) Função utilitária type-safe para checar flags
Agora criamos uma função checkedGet que recebe um config e uma chave, garantindo que a chave exista em tempo de compilação.
O que este bloco faz: fornece uma função simples e tipada para ler flags.
// checked-get.ts
import { FeatureConfig, FeatureKey } from './flags';
export function isEnabled(config: FeatureConfig, key: FeatureKey): boolean {
return Boolean(config[key]);
}
// Uso:
// const enabled = isEnabled(defaultFeatures, 'login'); // ✅ tipo seguro
O que acontece: isEnabled aceita apenas chaves válidas. Se você tentar isEnabled(defaultFeatures, 'unknown' as any) o compilador reclama.
3) Leitura de runtime com type guard
Quando a configuração vem do ambiente (process.env, JSON externo), precisamos validar os dados antes de tratá-los como FeatureConfig. Implementamos um type guard safeParseConfig.
O que este bloco faz: valida um objeto qualquer e afirma para o TS que ele é FeatureConfig se passar nas checagens.
// runtime-parse.ts
import { FEATURE_KEYS, FeatureConfig, FeatureKey } from './flags';
function isRecord(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' && obj !== null;
}
export function safeParseConfig(obj: unknown): FeatureConfig | null {
if (!isRecord(obj)) return null;
const result: Partial<FeatureConfig> = {};
for (const k of FEATURE_KEYS) {
const v = obj[k];
if (typeof v !== 'boolean') return null;
result[k] = v;
}
return result as FeatureConfig;
}
// Uso:
// const raw = JSON.parse(process.env.FEATURES_JSON || '{}');
// const cfg = safeParseConfig(raw) ?? defaultFeatures;
O que acontece: safeParseConfig faz checagens runtime e retorna FeatureConfig ou null. Isso evita lançar mão de casts inseguros no resto do código.
Exemplo prático completo
Vamos montar um exemplo que integra tudo: leitura de VAR ambiente, fallback, e uso em uma função que ativa uma rota ou uma mensagem. Incluirei também um decorador simples para demonstrar aplicação em classes (opcional para iniciantes).
Setup: carregar configuração em runtime
Contexto: este arquivo centraliza a carga da configuração. Ele usa process.env.FEATURES_JSON como fonte (comum em contêineres) e aplica safeParseConfig.
// config.ts
import { defaultFeatures } from './flags';
import { safeParseConfig } from './runtime-parse';
const raw = (() => {
try { return JSON.parse(process.env.FEATURES_JSON || '{}'); }
catch { return {}; }
})();
export const FEATURES = safeParseConfig(raw) ?? defaultFeatures;
// Agora FEATURES é garantidamente FeatureConfig
O que acontece: se a variável de ambiente é inválida, usamos o defaultFeatures. Do ponto de vista do resto da aplicação, FEATURES é um objeto tipado.
Uso em rota ou função
Contexto: função que retorna mensagem dependendo da flag. Mostra uso de isEnabled e tipos seguros.
// service.ts
import { FEATURES } from './config';
import { isEnabled } from './checked-get';
export function renderHomepage(userId: string): string {
if (isEnabled(FEATURES, 'betaDashboard')) {
return `User ${userId}: vendo beta dashboard`;
}
if (isEnabled(FEATURES, 'payments')) {
return `User ${userId}: acessar pagamentos`;
}
return `User ${userId}: homepage padrão`;
}
// Uso: console.log(renderHomepage('u123'));
O que acontece: compile-time garante que 'betaDashboard' existe. Você não perde autocompletar nem segurança.
Decorador opcional para métodos que exigem flag
Contexto: um decorador que bloqueia execução de método se a flag estiver off. Mostra integração com classes e type-safety por string literal keys.
// decorators.ts
import { isEnabled } from './checked-get';
import { FEATURES } from './config';
import { FeatureKey } from './flags';
export function RequiresFeature(key: FeatureKey) {
return function (_target: any, _prop: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!isEnabled(FEATURES, key)) {
throw new Error(`Feature '${key}' is disabled`);
}
return original.apply(this, args);
};
return descriptor;
};
}
// Uso:
// class Controller {
// @RequiresFeature('payments')
// processPayment() { /* ... */ }
// }
O que acontece: o decorador aceita apenas FeatureKey. Tentar RequiresFeature('nonexistent' as any) fará o TS reclamar durante o desenvolvimento.
Erros comuns, armadilhas e dicas práticas
Mesmo com tipagem, existem erros comuns que iniciantes cometem. Aqui estão os principais e como evitá-los.
1) Usar strings soltas em vez de tipos
Problema: usar 'payments' em vários lugares sem centralizar o tipo leva a typos. Solução: centralize keys com FEATURE_KEYS e derive o tipo FeatureKey. Evita duplicação e mantém autocompletar.
2) Fazer casts extensivos (as any / as FeatureConfig)
Problema: converter diretamente JSON para FeatureConfig com 'as' silencia erros. Solução: use type guards (como safeParseConfig) que validam cada campo. Se a fonte for externa, sempre valide.
3) Ignorar defaults
Problema: confiar apenas em uma fonte remota sem fallback causa falhas em deploy. Solução: mantenha defaultFeatures robusto e use-o como fallback explícito.
Regra prática: qualquer entrada externa deve ser validada antes de ser tratada como tipo seguro.
Dica de performance: as checagens em safeParseConfig são O(n) no número de flags; isso é trivial na prática. Se a sua configuração tiver centenas de flags, ainda assim o impacto é pequeno, mas você pode cachear o resultado.
Dica de organização: mantenha FEATURE_KEYS e defaultFeatures no mesmo arquivo de flags. Evite espalhar nomes por múltiplos locais.
Próximos passos para aprofundar
Depois de dominar essa base, explore tópicos que elevam a solução para produção: segmentação por usuário, rollout gradual, integração com UIs de controle e armazenamento centralizado.
Rollout gradual e estratégia por usuário
Para avançar, implemente uma estratégia para habilitar flags só para um subconjunto de usuários (por id, por grupo, por percentual). Use tipos para modelar estratégias e combine com type guards para validar parâmetros.
Exemplo de ideia: FeatureConfig pode ter não só booleanos, mas objetos com 'enabledForPercent' e 'enabledForUsers', mas isso exige um refinamento de tipos (union discriminado) para manter type-safety.
Integração com painel de controle
Conectar as flags a um painel web exige endpoints seguros que validem e gravem flags. No backend, mantenha as mesmas estruturas de tipo e revalide qualquer input antes de persistir.
Testing e mocks
Nos testes unitários, crie helpers que constroem FeatureConfig personalizados com tipos — por exemplo, makeFeatures({ payments: true }). Isso evita mocks mágicos e facilita testes determinísticos.
Resumo prático dos próximos passos:
- Adicionar estratégias de rollout (percentual, por usuário).
- Validar e versionar o esquema de flags para suportar mudanças sem quebrar clientes.
- Escrever testes que usam configurações explícitas e pequenas fábricas de flags.
Conclusão prática
Feature flags type-safe em TypeScript combinam a flexibilidade de configuração runtime com a segurança do compilador. A abordagem que vimos -- definir FEATURE_KEYS como literal, usar Record para o contrato, criar isEnabled e safeParseConfig, e eventualmente usar decorators -- fornece uma base robusta e de fácil adoção por iniciantes.
Vantagens imediatas: autocompletar, prevenção de typos, contratos claros entre módulos e menor custo ao refatorar. Para quem está começando, implemente a versão mínima (flags + default + isEnabled) e vá adicionando validação runtime e funcionalidades conforme ganha confiança.
Próximo passo imediato sugerido: copie os arquivos de exemplo, rode com ts-node e experimente alterar process.env.FEATURES_JSON para ver a validação em ação. Em seguida, adicione um rollout por usuário simples e escreva testes unitários para a função renderHomepage.
Se precisar, posso gerar um boilerplate com esses arquivos prontos e configurar scripts de build/test para rodar localmente.
Assuntos: TypeScript, feature-flags, generics, type-guards, configuration
Leia também
IDs nominais em TypeScript: branded types, type guards e generics passo a passo
Aprenda a proteger IDs em TypeScript com branded types, type guards e generics. Do jeito errado (string genérico) ao jeito certo (fábrica + validação), com…
Otimização de consultas PostgreSQL com Prisma: EXPLAIN ANALYZE, índices e pool de conexões
Guia avançado para otimizar consultas PostgreSQL com Prisma: como usar EXPLAIN ANALYZE, escolher índices (btree/gin/expressões/partial/include) e operar…
Sistema de plugins type-safe em TypeScript com decorators e generics
Implementação prática de um sistema de plugins type-safe em TypeScript usando decorators, generics e type guards. Padrões, armadilhas comuns e exemplos reais…
Converter JSON snake_case para camelCase com TypeScript e Generics
Aprenda passo a passo a converter JSON snake_case para camelCase usando TypeScript: funções runtime recursivas, tipos genéricos DeepCamelCase e boas práticas…
DTOs type-safe em TypeScript: decorators, generics e type guards passo a passo
Aprenda a transformar JSON em DTOs type-safe com TypeScript usando decorators, generics e type guards. Guia passo a passo para iniciantes: configuração…
Config type-safe em TypeScript: loader genérico com merges e type guards
Padrão prático para loaders de config em TypeScript: substitua casts por um loader genérico que aplica defaults, faz deep merge e valida com type guards…