Destino Dev

Blog

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:

  1. Adicionar estratégias de rollout (percentual, por usuário).
  2. Validar e versionar o esquema de flags para suportar mudanças sem quebrar clientes.
  3. 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