Uma análise técnica sobre dívida técnica, problemas de tooling e a alternativa idiomática do JavaScript.

Quem trabalha com TypeScript por tempo suficiente acaba percebendo que Enums são um corpo estranho dentro da linguagem. Eles se comportam de forma diferente da maioria das construções do TypeScript porque não são apenas uma ferramenta de tipagem: introduzem semântica em runtime.

Enquanto o TypeScript, por design, tenta ser uma camada de tipos apagável sobre o JavaScript, os Enums violam essa premissa. Eles sobrevivem à compilação e geram código executável — algo que, no ecossistema moderno de build (Vite, Turbopack, esbuild, SWC, edge runtimes, serverless), cobra um preço que nem sempre é evidente no curto prazo.

Este texto explora por que Enums tendem a gerar fricção técnica em projetos de médio e longo prazo, como isso se manifesta em tooling moderno e por que a comunidade tem convergido para padrões mais idiomáticos baseados em objetos as const.


1. O conflito fundamental: tipagem estrutural vs. tipagem nominal

Um dos pilares do TypeScript é o sistema de tipos estrutural. Em termos práticos: se algo tem a forma esperada, ele é aceito. Isso reduz acoplamento, facilita refactors e permite que tipos sejam usados como contratos, não como identidades rígidas.

Enums quebram exatamente essa lógica. Eles introduzem tipagem nominal: o tipo não é definido pelo valor, mas pela declaração específica do enum.

// Enum Clássico
enum Status {
  Pending = 'PENDING',
  Done = 'DONE'
}

// Isso falha, mesmo que a string seja idêntica
const myStatus: Status = 'PENDING'; 
// ❌ Error: Type '"PENDING"' is not assignable to type 'Status'.

Do ponto de vista do compilador, 'PENDING' não é um Status, mesmo sendo literalmente o mesmo valor. Isso força:

  • imports artificiais apenas para satisfazer o sistema de tipos
  • acoplamento entre camadas que poderiam ser independentes
  • fricção em contratos de API (HTTP, eventos, filas, mensageria)

Em sistemas distribuídos, isso é especialmente problemático. O backend não “envia um enum”, ele envia uma string. O frontend não “recebe um enum”, ele recebe dados. Enums forçam uma abstração que não existe no mundo real da aplicação.

Além disso, o output compilado de um enum padrão gera uma IIFE (Immediately Invoked Function Expression) verbosa, que ferramentas de tree-shaking nem sempre conseguem eliminar eficientemente:

// Output JS do Enum
var Status;
(function (Status) {
    Status["Pending"] = "PENDING";
    Status["Done"] = "DONE";
})(Status || (Status = {}));

Esse padrão:

  • adiciona código imperativo ao bundle
  • dificulta tree-shaking
  • aumenta o custo de inicialização do módulo
  • cria efeitos colaterais globais sutis

Tudo isso para representar algo que, semanticamente, é apenas um conjunto de valores possíveis.


2. const enum: uma otimização que conflita com o ecossistema moderno

À primeira vista, const enum parece a correção ideal: remove o código em runtime via inlining.

const enum Direction { Up, Down }
const move = Direction.Up;

// Compila para:
const move = 0; 

Embora resolva o problema do tamanho do bundle, o const enum introduz riscos significativos de Tooling.

Ferramentas modernas como Vite, esbuild e o compilador do Next.js (SWC) frequentemente operam em modo de "Compilação Isolada" (isolatedModules: true). Nesses ambientes, cada arquivo é transpilado independentemente.

Se você exporta um const enum em um arquivo e tenta usá-lo em outro, o transpilador pode não ter acesso à definição original durante a compilação isolada do arquivo consumidor. Isso resulta em erros de build ou comportamentos imprevisíveis, pois o valor literal não pode ser "inlinado" se a definição não estiver presente no mesmo contexto de compilação.

Para autores de bibliotecas, o risco é ainda maior: publicar tipos que dependem de const enums força o consumidor da sua lib a usar configurações específicas de compilador, o que é uma má prática de distribuição.


3. O padrão idiomático: objetos as const + extração de tipos

A abordagem que tem se consolidado como padrão na comunidade (frequentemente chamada de "POJO pattern") alinha-se melhor com a natureza do JavaScript. Utilizamos objetos simples e deixamos o TypeScript inferir os tipos literais.

Implementação

// 1. Definição do objeto imutável
export const UserRole = {
  ADMIN: 'admin',
  EDITOR: 'editor',
  GUEST: 'guest',
} as const;

// 2. Extração do tipo a partir dos valores
export type UserRole = (typeof UserRole)[keyof typeof UserRole];
// Resultado equivalente a: "admin" | "editor" | "guest"

Vantagens Técnicas

  1. Compatibilidade Universal: Como é apenas JavaScript válido com uma asserção de tipo, funciona perfeitamente com qualquer ferramenta de build (Babel, SWC, esbuild) sem configurações especiais.
  2. Tree-Shaking Real: Se você usar apenas o tipo UserRole e não o objeto runtime, nada será incluído no bundle final. Se usar o objeto, ele é apenas um objeto literal leve.
  3. Tipagem Estrutural:
    // Isso é válido com o padrão 'as const'
    const role: UserRole = 'admin'; 
    
    Isso permite maior flexibilidade. O frontend pode enviar uma string simples, e o backend valida se ela se encaixa no tipo UserRole sem precisar compartilhar a definição física do enum.
  4. Previsibilidade: Elimina comportamentos confusos dos enums numéricos do TypeScript (como reverse mappings onde Status[0] retorna a chave "PENDING").

Resumo Comparativo

Característica Enum Padrão const Enum Objeto as const
Runtime Overhead Alto (IIFE) Nulo (Idealmente) Baixo (Objeto simples)
Sistema de Tipos Nominal (Restritivo) Nominal Estrutural (Flexível)
Compatibilidade de Tooling Boa Problemática (Isolated Modules) Nativa
Tree-Shaking Complexo N/A (Inlined) Simples

Conclusão

Enums fazem mais sentido em linguagens onde tipos e runtime são a mesma coisa (como C# ou Java). No TypeScript, eles criam uma abstração híbrida que cobra um preço: mais acoplamento, mais exceções no tooling e mais complexidade acidental.

Para a maioria dos projetos modernos, especialmente aqueles rodando em stacks como Next.js ou utilizando monorepos, objetos as const oferecem a mesma segurança de tipos com menos "magia" de compilador e maior compatibilidade com o ecossistema. É uma solução mais alinhada com a filosofia de que o TypeScript deve ser apenas uma camada de tipos sobre o JavaScript, e não uma linguagem que muda fundamentalmente como o código opera em execução.


Referências:

TypeScript Documentation: isolatedModules

TypeScript Documentation: Enums

How const enums work