Explore alternativas poderosas aos enums no TypeScript: const assertions e union types. Saiba quando usar cada uma para um código robusto e manutenível.
Além dos Enums: Const Assertions vs. Union Types no TypeScript
No mundo do JavaScript com tipagem estática do TypeScript, os enums têm sido há muito tempo uma escolha para representar um conjunto fixo de constantes nomeadas. Eles oferecem uma maneira clara e legível de definir uma coleção de valores relacionados. No entanto, à medida que os projetos crescem e evoluem, os desenvolvedores frequentemente buscam alternativas mais flexíveis e, às vezes, mais performáticas. Dois contendores poderosos que frequentemente surgem são as const assertions e os union types. Este post aprofunda as nuances do uso dessas alternativas aos enums tradicionais, fornecendo exemplos práticos e orientando você sobre quando escolher cada uma.
Compreendendo os Enums Tradicionais do TypeScript
Antes de explorarmos as alternativas, é essencial ter uma compreensão firme de como os enums padrão do TypeScript funcionam. Enums permitem que você defina um conjunto de constantes numéricas ou de string nomeadas. Elas podem ser numéricas (o padrão) ou baseadas em string.
Enums Numéricos
Por padrão, os membros do enum recebem valores numéricos começando em 0.
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Saída: 0
Você também pode atribuir valores numéricos explicitamente.
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Saída: 200
Enums de String
Enums de string são frequentemente preferidos por sua experiência de depuração aprimorada, pois os nomes dos membros são preservados no JavaScript compilado.
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Saída: "BLUE"
O Overhead dos Enums
Embora os enums sejam convenientes, eles vêm com um leve overhead. Quando compilados para JavaScript, os enums do TypeScript são transformados em objetos que geralmente possuem mapeamentos reversos (por exemplo, mapeando o valor numérico de volta para o nome do enum). Isso pode ser útil, mas também contribui para o tamanho do bundle e pode nem sempre ser necessário.
Considere este simples enum de string:
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
Em JavaScript, isso pode se tornar algo como:
var Status;
(function (Status) {
Status["Pending"] = "PENDING";
Status["Processing"] = "PROCESSING";
Status["Completed"] = "COMPLETED";
})(Status || (Status = {}));
Para conjuntos simples e somente leitura de constantes, este código gerado pode parecer um pouco excessivo.
Alternativa 1: Const Assertions
Const assertions são um recurso poderoso do TypeScript que permite que você diga ao compilador para inferir o tipo mais específico possível para um valor. Quando usadas com arrays ou objetos destinados a representar um conjunto fixo de valores, elas podem servir como uma alternativa leve aos enums.
Const Assertions com Arrays
Você pode criar um array de literais de string e, em seguida, usar uma asserção const para tornar seu tipo imutável e seus elementos tipos literais.
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Erro: O tipo '"FAILED"' não é atribuível ao tipo 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Processando status: ${status}`);
}
processStatus("COMPLETED");
Vamos detalhar o que está acontecendo aqui:
as const: Esta asserção diz ao TypeScript para tratar o array como somente leitura e inferir os tipos literais mais específicos para seus elementos. Portanto, em vez destring[], o tipo se tornareadonly ["PENDING", "PROCESSING", "COMPLETED"].typeof statusArray[number]: Este é um tipo mapeado. Ele itera sobre todos os índices dostatusArraye extrai seus tipos literais. A assinatura de índicenumberessencialmente diz "me dê o tipo de qualquer elemento neste array". O resultado é um tipo de união:"PENDING" | "PROCESSING" | "COMPLETED".
Essa abordagem oferece segurança de tipo semelhante aos enums de string, mas gera um JavaScript mínimo. O próprio statusArray permanece um array de strings em JavaScript.
Const Assertions com Objetos
Const assertions são ainda mais poderosas quando aplicadas a objetos. Você pode definir um objeto onde as chaves representam suas constantes nomeadas e os valores são as strings literais ou números.
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Erro: O tipo '"GUEST"' não é atribuível ao tipo 'UserRole'.
function displayRole(role: UserRole) {
console.log(`A função do usuário é: ${role}`);
}
displayRole(userRoles.Admin); // Válido
displayRole("EDITOR"); // Válido
Neste exemplo de objeto:
as const: Esta asserção torna todo o objeto somente leitura. Mais importante, ela infere tipos literais para todos os valores de propriedade (por exemplo,"ADMIN"em vez destring) e torna as próprias propriedades somente leitura.keyof typeof userRoles: Esta expressão resulta em uma união das chaves do objetouserRoles, que é"Admin" | "Editor" | "Viewer".typeof userRoles[keyof typeof userRoles]: Este é um tipo de lookup. Ele pega a união das chaves e a usa para procurar os valores correspondentes no tipouserRoles. Isso resulta na união dos valores:"ADMIN" | "EDITOR" | "VIEWER", que é o nosso tipo desejado para funções.
A saída JavaScript para userRoles será um objeto JavaScript simples:
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
Isso é significativamente mais leve do que um enum típico.
Quando Usar Const Assertions
- Constantes somente leitura: Quando você precisa de um conjunto fixo de literais de string ou número que não devem ser alterados em tempo de execução.
- Saída mínima de JavaScript: Se você está preocupado com o tamanho do bundle e deseja a representação em tempo de execução mais performática para suas constantes.
- Estrutura semelhante a objeto: Quando você prefere a legibilidade de pares chave-valor, semelhante à forma como você pode estruturar dados ou configurações.
- Conjuntos baseados em string: Particularmente úteis para representar estados, tipos ou categorias que são melhor identificados por strings descritivas.
Alternativa 2: Union Types
Union types permitem que você declare que uma variável pode conter um valor de um de vários tipos. Quando combinados com tipos literais (literais de string, número, boolean), eles formam uma maneira poderosa de definir um conjunto de valores permitidos sem a necessidade de uma declaração explícita de constante para o conjunto em si.
Union Types com Literais de String
Você pode definir diretamente uma união de literais de string.
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Erro: O tipo '"BLUE"' não é atribuível ao tipo 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Mudando a luz para: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Erro
Esta é a maneira mais direta e muitas vezes a mais concisa de definir um conjunto de valores de string permitidos.
Union Types com Literais Numéricos
Da mesma forma, você pode usar literais numéricos.
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Erro: O tipo '201' não é atribuível ao tipo 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("Sucesso!");
} else {
console.log(`Código de erro: ${code}`);
}
}
handleResponse(500);
Quando Usar Union Types
- Conjuntos simples e diretos: Quando o conjunto de valores permitidos é pequeno, claro e não requer chaves descritivas além dos próprios valores.
- Constantes implícitas: Quando você não precisa se referir a uma constante nomeada para o conjunto em si, mas sim usar diretamente os valores literais.
- Máxima concisão: Para cenários diretos onde definir um objeto ou array dedicado parece excessivo.
- Parâmetros/tipos de retorno de função: Excelente para definir o conjunto exato de entradas/saídas de string ou número aceitáveis para funções.
Comparando Enums, Const Assertions e Union Types
Vamos resumir as principais diferenças e casos de uso:
Comportamento em Tempo de Execução
- Enums: Geram objetos JavaScript, potencialmente com mapeamento reverso.
- Const Assertions (Arrays/Objetos): Geram arrays ou objetos JavaScript simples. As informações de tipo são apagadas em tempo de execução, mas a estrutura de dados permanece.
- Union Types (com literais): Nenhuma representação em tempo de execução para a união em si. Os valores são apenas literais. A verificação de tipo ocorre puramente em tempo de compilação.
Legibilidade e Expressividade
- Enums: Alta legibilidade, especialmente com nomes descritivos. Podem ser mais verbosos.
- Const Assertions (Objetos): Boa legibilidade através de pares chave-valor, imitando configurações ou definições.
- Const Assertions (Arrays): Menos legível para representar constantes nomeadas, mais para uma lista ordenada de valores.
- Union Types: Muito concisos. A legibilidade depende da clareza dos próprios valores literais.
Segurança de Tipo
- Todas as três abordagens oferecem forte segurança de tipo. Elas garantem que apenas valores válidos e predefinidos possam ser atribuídos a variáveis ou passados para funções.
Tamanho do Bundle
- Enums: Geralmente o maior devido aos objetos JavaScript gerados.
- Const Assertions: Menor que enums, pois produzem estruturas de dados simples.
- Union Types: O menor, pois não geram nenhuma estrutura de dados específica em tempo de execução para o tipo em si, dependendo apenas de valores literais.
Matriz de Casos de Uso
Aqui está um guia rápido:
| Recurso | Enum do TypeScript | Const Assertion (Objeto) | Const Assertion (Array) | Union Type (Literais) |
|---|---|---|---|---|
| Saída em Tempo de Execução | Objeto JS (com mapeamento reverso) | Objeto JS Simples | Array JS Simples | Nenhuma (apenas valores literais) |
| Legibilidade (Constantes Nomeadas) | Alta | Alta | Média | Baixa (valores são nomes) |
| Tamanho do Bundle | Maior | Médio | Médio | Menor |
| Flexibilidade | Boa | Boa | Boa | Excelente (para conjuntos simples) |
| Uso Comum | Estados, Códigos de Status, Categorias | Configurações, Definições de Funções, Flags de Recursos | Listas ordenadas de valores imutáveis | Parâmetros de função, valores restritos simples |
Exemplos Práticos e Melhores Práticas
Exemplo 1: Representando Códigos de Status de API
Enum:
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Const Assertion (Objeto):
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Union Type:
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... lógica ...
}
Recomendação: Para este cenário, um union type é frequentemente o mais conciso e eficiente. Os próprios valores literais são descritivos o suficiente. Se você precisasse associar metadados adicionais a cada status (por exemplo, uma mensagem amigável para o usuário), um objeto com const assertion seria uma escolha melhor.
Exemplo 2: Definindo Funções de Usuário
Enum:
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... lógica ...
}
Const Assertion (Objeto):
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... lógica ...
}
Union Type:
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... lógica ...
}
Recomendação: Um objeto com const assertion atinge um bom equilíbrio aqui. Ele fornece pares chave-valor claros (por exemplo, userRolesObject.Admin) que podem melhorar a legibilidade ao referenciar funções, ao mesmo tempo em que permanece performático. Um union type também é um forte candidato se literais de string diretos forem suficientes.
Exemplo 3: Representando Opções de Configuração
Imagine um objeto de configuração para uma aplicação global que pode ter temas diferentes.
Enum:
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... outras opções de configuração ...
}
Const Assertion (Objeto):
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... outras opções de configuração ...
}
Union Type:
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... outras opções de configuração ...
}
Recomendação: Para configurações como temas, o objeto com const assertion é frequentemente ideal. Ele define claramente as opções disponíveis e seus valores de string correspondentes. As chaves (Light, Dark, System) são descritivas e mapeiam diretamente para os valores, tornando o código de configuração muito compreensível.
Escolhendo a Ferramenta Certa para o Trabalho
A decisão entre enums do TypeScript, const assertions e union types nem sempre é clara. Muitas vezes, resume-se a um trade-off entre performance em tempo de execução, tamanho do bundle e legibilidade/expressividade do código.
- Opte por Union Types quando precisar de um conjunto simples e restrito de literais de string ou número e a máxima concisão for desejada. Eles são excelentes para assinaturas de função e restrições de valores básicas.
- Opte por Const Assertions (com Objetos) quando desejar uma maneira mais estruturada e legível de definir constantes nomeadas, semelhante a um enum, mas com sobrecarga de tempo de execução significativamente menor. Isso é ótimo para configurações, funções ou qualquer conjunto onde as chaves adicionam significado significativo.
- Opte por Const Assertions (com Arrays) quando você simplesmente precisa de uma lista ordenada imutável de valores, e o acesso direto por índice é mais importante do que chaves nomeadas.
- Considere TypeScript Enums quando precisar de seus recursos específicos, como mapeamento reverso (embora isso seja menos comum no desenvolvimento moderno) ou se sua equipe tiver uma forte preferência e o impacto na performance for insignificante para o seu projeto.
Em muitos projetos modernos do TypeScript, você encontrará uma tendência em direção a const assertions e union types em vez de enums tradicionais, especialmente para constantes baseadas em string, devido às suas melhores características de performance e saída JavaScript frequentemente mais simples.
Considerações Globais
Ao desenvolver aplicações para um público global, definições de constantes consistentes e previsíveis são cruciais. As escolhas que discutimos (enums, const assertions, union types) contribuem para essa consistência, impondo segurança de tipo em diferentes ambientes e localidades de desenvolvedores.
- Consistência: Independentemente do método escolhido, o principal é a consistência dentro do seu projeto. Se você decidir usar objetos com const assertion para funções, mantenha esse padrão em toda a base de código.
- Internacionalização (i18n): Ao definir rótulos ou mensagens que serão internacionalizadas, use essas estruturas type-safe para garantir que apenas chaves ou identificadores válidos sejam usados. As strings traduzidas reais serão gerenciadas separadamente por meio de bibliotecas de i18n. Por exemplo, se você tiver um campo `status` que pode ser "PENDING", "PROCESSING", "COMPLETED", sua biblioteca de i18n mapeará esses identificadores internos para texto de exibição localizado.
- Fuso Horário e Moedas: Embora não estejam diretamente relacionados a enums, lembre-se de que, ao lidar com valores como datas, horários ou moedas, o sistema de tipos do TypeScript pode ajudar a impor o uso correto, mas bibliotecas externas geralmente são necessárias para tratamento global preciso. Por exemplo, um tipo de união `Currency` poderia ser definido como
"USD" | "EUR" | "GBP", mas a lógica de conversão real requer ferramentas especializadas.
Conclusão
O TypeScript oferece um rico conjunto de ferramentas para gerenciar constantes. Embora os enums tenham nos servido bem, as const assertions e os union types oferecem alternativas atraentes e frequentemente mais performáticas. Ao entender suas diferenças e escolher a abordagem certa com base em suas necessidades específicas — seja performance, legibilidade ou concisão — você pode escrever código TypeScript mais robusto, manutenível e eficiente que escala globalmente.
Adotar essas alternativas pode levar a tamanhos de bundle menores, aplicações mais rápidas e uma experiência de desenvolvedor mais previsível para sua equipe internacional.