Saltar al contenido

Enums: la forma que el otro lado puede discriminar

Un enum de Rust con datos tiene cuatro representaciones posibles en JSON y la que sale por defecto es la peor para el frontend. Cuál elegir, cómo se ve cada una, y por qué esto decide si tu TypeScript puede razonar.

El enum de partida

Rust
#[derive(serde::Serialize)]pub enum Estado {    Cargando,    Listo { ficheros: usize },    Fallo { code: String, message: String },}

Las cuatro representaciones

La que quieres, casi siempre

Rust
#[derive(serde::Serialize)]#[serde(tag = "tipo", rename_all = "camelCase")]pub enum Estado {    Cargando,    Listo { ficheros: usize },    Fallo { code: String, message: String },}
JSON
{"tipo":"cargando"}{"tipo":"listo","ficheros":128}{"tipo":"fallo","code":"IO","message":"no se pudo leer"}

Y al otro lado, sin escribir nada más:

TypeScript
type Estado =  | { tipo: 'cargando' }  | { tipo: 'listo'; ficheros: number }  | { tipo: 'fallo'; code: string; message: string };// El compilador de TypeScript estrecha el tipo solo:if (e.tipo === 'listo') {  console.log(e.ficheros);   // aquí SABE que existe}

Esa es la propiedad que se compra: el compilador del frontend puede razonar sobre los estados de tu dominio. Con la representación por defecto, no.

Cuándo hace falta la adyacente

La interna tiene una limitación real: no puede representar una variante cuyos datos no sean un objeto. Si una variante lleva un valor suelto —una cadena, un número, una lista— no hay dónde poner el campo del discriminante junto a él.

Rust
// La interna NO puede con esto: la variante lleva un valor, no campos.pub enum Mensaje {    Texto(String),    Numeros(Vec<u32>),}

Para ese enum hace falta la adyacente, que separa las dos cosas:

JSON
{"tipo":"texto","datos":"hola"}{"tipo":"numeros","datos":[1,2,3]}

La alternativa que suele ser mejor: convertir las variantes de valor suelto en variantes con campos con nombre. Texto { texto: String } en vez de Texto(String). Cuesta una palabra y deja usar la interna, que es la más limpia al otro lado. Y de paso el campo tiene nombre, que es información que antes no estaba.

El caso fácil: enums sin datos

Si ninguna variante lleva datos, no hay nada que decidir: sale como una cadena.

Rust
#[derive(serde::Serialize)]#[serde(rename_all = "camelCase")]pub enum Orden { PorNombre, PorFecha, PorTamano }
JSON
"porNombre"

Y en TypeScript es una unión de literales, que es exactamente lo que quieres. Es el caso que no necesita ningún atributo de etiquetado, solo el de renombrado si quieres la convención del frontend.