Tipos literais e enums

1. Introdução aos Tipos Literais

Em TypeScript, tipos literais permitem especificar exatamente quais valores são permitidos para uma variável ou parâmetro. Diferentemente de tipos amplos como string ou number, um tipo literal restringe o valor a um conjunto específico de opções.

A diferença fundamental entre valores literais e tipos literais é sutil mas importante: enquanto "azul" é um valor literal do tipo string, "azul" como tipo literal significa que apenas a string exata "azul" é aceita.

type Direcao = "norte" | "sul" | "leste" | "oeste";

let direcao: Direcao = "norte"; // OK
direcao = "sudoeste"; // Erro: Type '"sudoeste"' is not assignable to type 'Direcao'

2. Tipos Literais em Ação

Os tipos literais brilham quando combinados com union types, permitindo criar restrições precisas para funções e variáveis.

function mover(direcao: "norte" | "sul" | "leste" | "oeste", passos: number): void {
    console.log(`Movendo ${passos} passos para ${direcao}`);
}

mover("norte", 5); // OK
mover("nordeste", 3); // Erro de compilação

O TypeScript realiza narrowing automaticamente com base em verificações condicionais:

type Resposta = "sim" | "nao" | "talvez";

function processarResposta(resposta: Resposta): string {
    if (resposta === "sim") {
        return "Usuário confirmou";
    } else if (resposta === "nao") {
        return "Usuário recusou";
    }
    return "Resposta pendente";
}

3. Enums Numéricos

Enums numéricos são uma forma de agrupar constantes relacionadas com valores numéricos auto-incrementados.

enum Status {
    Ativo,    // 0
    Inativo,  // 1
    Pendente  // 2
}

console.log(Status.Ativo); // 0
console.log(Status[0]);    // "Ativo" (reverse mapping)

É possível atribuir valores personalizados:

enum NivelAcesso {
    Admin = 1,
    Usuario = 5,
    Convidado = 10
}

console.log(NivelAcesso.Admin); // 1

O reverse mapping permite acessar o nome a partir do valor numérico:

enum Cor {
    Vermelho = 1,
    Verde = 2,
    Azul = 3
}

const nomeCor = Cor[2]; // "Verde"

4. Enums de String

Enums de string oferecem maior legibilidade e são mais amigáveis para debug:

enum Cor {
    Vermelho = "VERMELHO",
    Verde = "VERDE",
    Azul = "AZUL"
}

function pintar(cor: Cor): void {
    console.log(`Pintando com a cor ${cor}`);
}

pintar(Cor.Vermelho); // "Pintando com a cor VERMELHO"

Quando usar union de literais vs enums de string:

  • Union de literais: Prefira quando precisar de simplicidade e não precisar de reverse mapping
  • Enums de string: Use quando precisar de agrupamento semântico ou quando o enum for parte de uma API pública
// Union de literais (mais simples)
type TamanhoPizza = "pequena" | "media" | "grande";

// Enum de string (mais semântico)
enum TamanhoPizzaEnum {
    Pequena = "PEQUENA",
    Media = "MEDIA",
    Grande = "GRANDE"
}

5. Enums Heterogêneos e Const Enums

Enums heterogêneos misturam strings e números, mas são desencorajados por tornarem o código confuso:

enum Misturado {
    Sim = 1,
    Nao = "NAO" // Permitido, mas não recomendado
}

const enum é uma otimização que elimina o objeto enum em tempo de compilação:

const enum Direcoes {
    Norte = "NORTE",
    Sul = "SUL",
    Leste = "LESTE",
    Oeste = "OESTE"
}

let dir = Direcoes.Norte; // Compila para: let dir = "NORTE";

Diferenças principais:
- enum gera código JavaScript com objeto e reverse mapping
- const enum é totalmente inline, sem geração de código objeto
- const enum não permite reverse mapping

6. Enums com Membros Computados

Membros constantes são avaliados em tempo de compilação, enquanto membros computados requerem expressões:

enum Tamanho {
    Pequeno = 1,
    Medio = Pequeno * 2,     // Membro computado: 2
    Grande = Medio * 2,      // Membro computado: 4
    ExtraGrande = Grande + 2 // Membro computado: 6
}

console.log(Tamanho.Medio); // 2

Restrições importantes:
- Membros computados não podem ser usados com const enum
- Expressões devem ser avaliáveis em tempo de compilação
- Evite lógica complexa em membros computados

7. Comparação: Tipos Literais vs. Enums

Cenários para tipos literais:
- Simplicidade e legibilidade
- Union types pequenos (2-5 valores)
- Quando não precisa de reverse mapping
- Em bibliotecas onde enums podem causar problemas de compatibilidade

type Estado = "carregando" | "sucesso" | "erro";

Cenários para enums:
- Agrupamento semântico de constantes relacionadas
- Reverse mapping necessário
- APIs públicas que exigem nomes descritivos
- Quando valores precisam ser auto-incrementados

enum HttpStatus {
    OK = 200,
    NotFound = 404,
    InternalError = 500
}

Trade-offs:
- Enums geram código JavaScript adicional
- Tipos literais são mais leves em runtime
- Enums oferecem melhor autocompletação em IDEs
- Union de literais são mais previsíveis para tipos

8. Boas Práticas e Padrões Avançados

Type guards com enums:

enum Fruta {
    Maca = "MAÇA",
    Banana = "BANANA",
    Laranja = "LARANJA"
}

function isFruta(valor: string): valor is Fruta {
    return Object.values(Fruta).includes(valor as Fruta);
}

Extraindo tipos de enums com keyof e typeof:

enum DiasSemana {
    Segunda = "SEG",
    Terca = "TER",
    Quarta = "QUA",
    Quinta = "QUI",
    Sexta = "SEX"
}

type ChavesDias = keyof typeof DiasSemana; // "Segunda" | "Terca" | "Quarta" | "Quinta" | "Sexta"
type ValoresDias = `${DiasSemana}`; // "SEG" | "TER" | "QUA" | "QUI" | "SEX"

Recomendações para APIs:
- Prefira union de literais em APIs internas
- Use enums em APIs públicas quando precisar de documentação explícita
- Considere const enum para otimização em projetos grandes
- Documente claramente os valores esperados

// Boa prática: documentar enums em APIs
/**
 * Representa os status possíveis de uma transação
 */
enum StatusTransacao {
    /** Transação iniciada */
    Pendente = "PENDENTE",
    /** Transação processada com sucesso */
    Confirmada = "CONFIRMADA",
    /** Transação cancelada pelo usuário */
    Cancelada = "CANCELADA"
}

Referências