Enums, uniones de literales y objetos as const en TypeScript
En esta lección
Muchos datos de un programa solo pueden tomar unos pocos valores fijos: el estado de un pedido, la talla de una camiseta, el nivel de dificultad de una ruta o el rol de un usuario. TypeScript ofrece varias formas de representar estos conjuntos cerrados de valores. En esta lección verás la clásica, los enums, y las dos alternativas que hoy prefiere mucha gente: las uniones de literales y los objetos as const. Al final sabrás cuándo conviene cada una.
El problema: textos sueltos
Imagina una tienda donde cada pedido tiene un estado. Con JavaScript lo normal sería usar textos:
function avisar(estado: string): string {
if (estado === "enviado") return "Tu pedido va de camino";
return "Seguimos preparando tu pedido";
}
console.log(avisar("enviao")); // Seguimos preparando tu pedido
La errata "enviao" pasa sin que nadie se entere, porque string acepta cualquier texto. Lo que queremos es decirle a TypeScript: “el estado solo puede ser uno de estos valores”. Veamos las herramientas.
Enums numéricos
Un enum (de enumeración) es una lista de constantes con nombre. Si no les das valor, TypeScript las numera desde 0:
enum Talla {
S,
M,
L,
XL,
}
let talla: Talla = Talla.M;
console.log(talla); // 1
console.log(Talla.XL); // 3
console.log(Talla[2]); // L
talla = 7;
// error TS2322: Type '7' is not assignable to type 'Talla'.
Tallaes a la vez un tipo (para anotar variables) y un objeto que existe en ejecución.- Se accede a cada miembro con el punto:
Talla.M. - Los enums numéricos tienen mapeo inverso:
Talla[2]devuelve el nombre"L". Es útil para mostrar el nombre de un valor guardado como número.
Puedes fijar el valor de inicio o de cada miembro. Los que no tienen valor continúan contando desde el anterior:
enum Prioridad {
Baja = 1,
Media,
Alta,
Urgente = 10,
}
console.log(Prioridad.Media); // 2
console.log(Prioridad.Alta); // 3
console.log(Prioridad.Urgente); // 10
Cuidado: un enum numérico acepta cualquier valor de tipo
numberque venga de una variable, aunque no sea ninguno de sus miembros. Si leesconst n: number = 99de una base de datos,const p: Prioridad = ncompila sin quejarse. Por eso los enums numéricos dan menos seguridad de la que parece.
Enums de texto
En un enum de texto cada miembro tiene un valor de tipo string que escribes tú:
enum EstadoPedido {
Pendiente = "PENDIENTE",
Pagado = "PAGADO",
Enviado = "ENVIADO",
Entregado = "ENTREGADO",
}
function avisar(estado: EstadoPedido): string {
switch (estado) {
case EstadoPedido.Pendiente:
return "Esperando el pago";
case EstadoPedido.Pagado:
return "Preparando tu pedido";
case EstadoPedido.Enviado:
return "Tu pedido va de camino";
case EstadoPedido.Entregado:
return "Pedido entregado";
}
}
console.log(avisar(EstadoPedido.Enviado)); // Tu pedido va de camino
console.log(EstadoPedido.Pagado); // PAGADO
Ventajas frente a los numéricos: el valor se entiende al leerlo en la consola, en un JSON o en la base de datos ("PAGADO" dice más que 1), y no aceptan números al azar. No tienen mapeo inverso.
Hay un detalle que sorprende. Aunque el valor de EstadoPedido.Pagado es el texto "PAGADO", no puedes pasar el texto directamente:
enum EstadoPedido {
Pendiente = "PENDIENTE",
Pagado = "PAGADO",
}
function cobrar(estado: EstadoPedido): void {}
cobrar(EstadoPedido.Pagado); // bien
cobrar("PAGADO");
// error TS2345: Argument of type '"PAGADO"' is not assignable to parameter of type 'EstadoPedido'.
Los enums son nominales: solo valen los miembros del propio enum. Eso es incómodo cuando los datos llegan de fuera (un formulario, una API), porque siempre son textos normales y tendrás que convertirlos.
Qué genera un enum al compilar
Casi todo lo que añade TypeScript desaparece al compilar: las anotaciones de tipo, las interfaces, los alias. Los enums no. Este enum:
enum Nivel {
Facil = "FACIL",
Dificil = "DIFICIL",
}
se convierte en este JavaScript:
var Nivel;
(function (Nivel) {
Nivel["Facil"] = "FACIL";
Nivel["Dificil"] = "DIFICIL";
})(Nivel || (Nivel = {}));
Es decir, un enum genera código. Esto importa más de lo que parece, como verás enseguida.
const enum
Si escribes const enum, TypeScript no genera el objeto: sustituye cada uso por su valor al compilar.
const enum Direccion {
Arriba = 1,
Abajo,
}
console.log(Direccion.Abajo); // 2
El JavaScript resultante es solo console.log(2 /* Direccion.Abajo */);. Ahorra unos bytes, pero tiene inconvenientes: no puedes recorrer sus miembros, no tienes mapeo inverso y da problemas con herramientas que compilan archivo por archivo (Vite, esbuild, la opción isolatedModules). Hoy se desaconseja salvo en casos muy concretos.
Alternativa 1: uniones de literales
Ya conoces los tipos literales y las uniones. Una unión de textos resuelve el problema del principio sin generar nada de código:
type Dificultad = "facil" | "media" | "dificil";
function duracionEstimada(km: number, dificultad: Dificultad): number {
const ritmo = dificultad === "facil" ? 4 : dificultad === "media" ? 3 : 2;
return Math.round((km / ritmo) * 10) / 10;
}
console.log(duracionEstimada(12, "media")); // 4
duracionEstimada(12, "dificl");
// error TS2345: Argument of type '"dificl"' is not assignable to parameter of type 'Dificultad'.
- El editor te autocompleta los valores posibles al escribir las comillas.
- Puedes pasar un texto normal, como
"media", sin conversiones. Encaja muy bien con datos que vienen de JSON. - Al compilar desaparece: el JavaScript es exactamente el que escribirías a mano.
Su única limitación es que no existe en ejecución: no hay ningún objeto que puedas recorrer para, por ejemplo, rellenar un <select> con todas las dificultades.
Alternativa 2: objetos as const
Para tener los valores también en ejecución, se usa un objeto normal de JavaScript con as const. Esa aserción le dice a TypeScript que el objeto es de solo lectura y que guarde los valores exactos ("PAGADO") en lugar del tipo general string:
const Rol = {
Admin: "admin",
Editor: "editor",
Lector: "lector",
} as const;
type Rol = (typeof Rol)[keyof typeof Rol];
// type Rol = "admin" | "editor" | "lector"
function puedeEditar(rol: Rol): boolean {
return rol === Rol.Admin || rol === Rol.Editor;
}
console.log(puedeEditar(Rol.Editor)); // true
console.log(puedeEditar("lector")); // false
console.log(Object.values(Rol)); // [ 'admin', 'editor', 'lector' ]
La línea del tipo parece un jeroglífico, así que vamos por partes:
| Expresión | Qué obtiene |
|---|---|
typeof Rol | El tipo del objeto: { readonly Admin: "admin"; readonly Editor: "editor"; readonly Lector: "lector" } |
keyof typeof Rol | La unión de sus claves: "Admin" | "Editor" | "Lector" |
(typeof Rol)[keyof typeof Rol] | La unión de sus valores: "admin" | "editor" | "lector" |
Con este patrón tienes lo mejor de los dos mundos: escribes Rol.Admin como con un enum, aceptas textos normales como con una unión, y puedes recorrer los valores con Object.values. Darle al tipo el mismo nombre que a la constante es legal (uno vive en el mundo de los tipos y otro en el de los valores) y es la convención habitual. Verás keyof y typeof con más detalle en tipos utilitarios.
Si solo te hacen falta las claves, keyof typeof basta:
const precios = {
basica: 25,
completa: 40,
familiar: 65,
} as const;
type Tarifa = keyof typeof precios; // "basica" | "completa" | "familiar"
function precioDe(tarifa: Tarifa): number {
return precios[tarifa];
}
console.log(precioDe("familiar")); // 65
Así, si mañana añades una tarifa al objeto, el tipo Tarifa se actualiza solo.
Por qué muchos proyectos evitan los enums
Los enums son de las pocas piezas de TypeScript que no son solo tipos: añaden código que no existe en JavaScript. Durante años eso daba igual, porque todo pasaba por tsc. Pero el ecosistema ha cambiado:
- Node.js ejecuta TypeScript directamente. Desde Node 22.18 y 23.6 puedes hacer
node app.ts. Node no compila: simplemente borra las anotaciones de tipo (type stripping). Lo que no se puede borrar sin más, como un enum, da error:
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
- La opción
erasableSyntaxOnly(TypeScript 5.8 o posterior) hace quetscmarque como error todo lo que no se pueda borrar: enums,namespacecon código y parameter properties de las clases. Así te aseguras de que tu código funciona con Node y con otras herramientas que solo eliminan tipos.
enum Color {
Rojo,
Verde,
}
// Con erasableSyntaxOnly:
// error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
- Además hay una propuesta para añadir anotaciones de tipo al propio JavaScript, y solo contempla la sintaxis “borrable”. Los enums se quedarían fuera.
Nada de esto significa que los enums estén prohibidos: funcionan, están en muchísimo código (Angular los usa a menudo) y los vas a leer. Pero si empiezas un proyecto hoy, las uniones de literales y los objetos as const hacen lo mismo con JavaScript estándar.
Comparativa
| Unión de literales | Objeto as const | Enum de texto | Enum numérico | |
|---|---|---|---|---|
| Genera código | No | Solo el objeto (JS normal) | Sí | Sí |
| Existe en ejecución (recorrer valores) | No | Sí | Sí | Sí, con mapeo inverso |
Acepta textos normales ("admin") | Sí | Sí | No | No |
| Autocompletado | Sí | Sí | Sí | Sí |
Funciona con erasableSyntaxOnly y node app.ts | Sí | Sí | No | No |
| Seguridad de tipos | Alta | Alta | Alta | Media (acepta cualquier number) |
Regla práctica:
- Unión de literales por defecto, para parámetros y propiedades con pocos valores.
- Objeto
as constcuando además necesitas recorrer los valores en ejecución (un desplegable, una validación). - Enum cuando el proyecto ya los usa o el framework los espera. Si lo haces, prefiere los de texto.
Errores frecuentes
- Guardar enums numéricos en la base de datos o en JSON: si alguien añade un miembro en medio, todos los números siguientes cambian y los datos guardados pasan a significar otra cosa. Usa enums de texto o uniones.
- Pasar un texto donde se espera un enum:
cobrar("PAGADO")no compila; necesitasEstadoPedido.Pagadoo convertir el valor tras validarlo. - Olvidar
as const: sin él,{ Admin: "admin" }tiene tipo{ Admin: string }, ytypeof Rol[keyof typeof Rol]acaba siendostring, que no protege de nada. - Confundir
keyof typeofcontypeof ...[keyof typeof ...]: el primero da las claves ("Admin"), el segundo los valores ("admin"). - Usar
const enumen proyectos con Vite oisolatedModules: puede romper la compilación por archivos. Mejor un objetoas const.
Resumen
| Concepto | Ejemplo |
|---|---|
| Enum numérico | enum Talla { S, M, L } (0, 1, 2) |
| Valores propios | enum Prioridad { Baja = 1, Media } |
| Mapeo inverso | Talla[1] → "M" (solo numéricos) |
| Enum de texto | enum Estado { Pagado = "PAGADO" } |
const enum | Se sustituye por el valor al compilar; desaconsejado |
| Unión de literales | type Dificultad = "facil" | "media" |
Objeto as const | const Rol = { Admin: "admin" } as const |
| Valores de un objeto como tipo | (typeof Rol)[keyof typeof Rol] |
| Claves de un objeto como tipo | keyof typeof precios |
erasableSyntaxOnly | Prohíbe enums y otras sintaxis que generan código |
En la siguiente lección aprenderás a escribir funciones, interfaces y clases que sirven para cualquier tipo sin perder la información de tipos: los genéricos.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.