wandres.dev
ENUMS AVANZADOS · modelar con precisión

Raw values y CaseIterable: el dominio como dato

Enums respaldados por String o Int, el protocolo RawRepresentable como biyección parcial, la síntesis de allCases y los riesgos de que un valor crudo se convierta en contrato serializado.

⏱ 16 min

Un enum describe un dominio cerrado; una cadena o un entero describen un dominio abierto. Los raw values son el puente entre ambos, y todo puente tiene dos direcciones con propiedades muy distintas: salir del enum siempre funciona, entrar puede fallar. Esa asimetría, formalizada en RawRepresentable, es la frontera donde tu programa valida el mundo exterior.

🎯 Al terminar esta lección sabrás
  • Entender RawRepresentable como una función total hacia fuera y parcial hacia dentro.
  • Conocer las reglas de síntesis de valores implícitos y sus trampas de compatibilidad.
  • Usar y personalizar CaseIterable, incluida la conformidad manual.
  • Componer raw values, Codable y allCases para validar entradas externas.

RawRepresentable: una biyección parcial

Declarar un tipo crudo hace que el compilador sintetice la conformidad con RawRepresentable, un protocolo con dos requisitos que no son simétricos.

enum Prioridad: Int {
    case baja = 0, media = 1, alta = 2
}

let n = Prioridad.alta.rawValue        // 2        siempre funciona
let p = Prioridad(rawValue: 7)         // nil      puede fallar

La propiedad rawValue es una función total: todo caso tiene su valor crudo. El inicializador es parcial: la mayoría de los enteros no corresponden a ningún caso, por eso devuelve un opcional. La conversión hacia dentro es, literalmente, el acto de validar: convierte un dominio infinito y sin garantías en un dominio finito y comprobado.

El tipo crudo debe ser Equatable y expresable por literal, lo que en la práctica significa String, los enteros, los flotantes o el propio Character. Los valores deben ser únicos y ningún caso puede tener valores asociados: un caso con payload no tiene un valor crudo único que lo represente, así que raw values y payloads se excluyen mutuamente.

💡
Nada te obliga a usar la síntesis

Puedes conformar RawRepresentable a mano con cualquier tipo crudo, incluido uno propio. Es la técnica detrás de los identificadores fuertemente tipados de la biblioteca estándar y de los “enums abiertos” de las API de Apple: un struct que envuelve un String, con constantes estáticas como casos conocidos, y que acepta valores desconocidos sin romperse cuando el servidor inventa uno nuevo.

Valores implícitos y su fragilidad

Si no los escribes, el compilador los rellena. Con Int, empieza en cero y va incrementando desde el último valor explícito. Con String, usa el nombre del caso tal cual.

enum Estado: String {
    case pendiente          // "pendiente"
    case enCurso            // "enCurso"     ojo al camelCase
    case completado         // "completado"
}

enum Codigo: Int {
    case ok = 200
    case creado             // 201, continua desde el anterior
    case noEncontrado = 404
}

Esa comodidad esconde un peligro serio. En cuanto un raw value cruza una frontera de persistencia —un JSON, una fila de base de datos, un fichero de preferencias— deja de ser un detalle interno y pasa a ser contrato público. Renombrar enCurso a enProgreso es una refactorización que el compilador aplaude y que, en silencio, invalida todos los datos ya guardados.

⚠️
Escribe siempre el raw value si se serializa

Para cualquier enum que se guarde o se envíe por la red, declara los valores crudos de forma explícita, aunque coincidan con el nombre del caso. Así el nombre queda libre para evolucionar con el estilo del código y el valor persistido queda anclado como lo que es: un identificador estable que solo se cambia con una migración.

flowchart LR
A[Mundo exterior: JSON, base de datos, red] --> B[init con rawValue]
B -- coincide --> C[Caso valido del enum]
B -- no coincide --> D[nil: decision explicita]
C --> E[Logica de dominio con switch exhaustivo]
D --> F[Valor por defecto o error de decodificacion]
C --> G[rawValue de vuelta al exterior]
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b
style B fill:#89b4fa,color:#11111b

CaseIterable: el dominio como colección

Conformar CaseIterable sintetiza allCases, una colección con todos los casos en orden de declaración. La síntesis solo es automática si ningún caso tiene valores asociados, por la razón obvia: un caso con payload representa infinitos valores y no se puede enumerar.

enum Tema: String, CaseIterable {
    case claro = "light", oscuro = "dark", sistema = "system"
}

Tema.allCases.count                                  // 3
Tema.allCases.map(\.rawValue)                        // ["light", "dark", "system"]

Conviene recordar dos detalles. El primero es que allCases es una propiedad calculada: el tipo asociado AllCases se sintetiza como un Array que se construye en cada acceso, así que invocarla dentro de un bucle caliente asigna memoria una y otra vez; guárdala en una constante estática si el recorrido es frecuente. El segundo es que su orden es el de declaración, lo que la convierte en una fuente estable para menús y tablas, pero también en algo que un reordenamiento inocente del código puede alterar.

Cuando la síntesis no aplica, la conformidad manual sigue disponible: basta con proporcionar allCases tú mismo, incluso enumerando combinaciones finitas de payloads.

enum Palo: CaseIterable { case picas, corazones, diamantes, treboles }

enum Carta: CaseIterable {
    case numero(Int, Palo)
    case figura(String, Palo)

    static var allCases: [Carta] {
        Palo.allCases.flatMap { palo in
            (2...10).map { Carta.numero($0, palo) }
            + ["J", "Q", "K", "A"].map { Carta.figura($0, palo) }
        }
    }
}
Enumerar el dominio es poder demostrar sobre él

allCases parece una utilidad para rellenar un menú desplegable, y ahí es donde casi todo el mundo lo deja. Su valor real es epistemológico: cuando puedes recorrer la totalidad de un dominio, dejas de razonar con ejemplos y empiezas a razonar con exhaustividad. Un test que recorre allCases y comprueba que init con rawValue devuelve el mismo caso no verifica una muestra: demuestra la propiedad de ida y vuelta para todo el tipo, con una cobertura que ningún conjunto de casos de prueba escritos a mano puede igualar y que se amplía sola cuando alguien añade un caso. Lo mismo vale para comprobar que cada caso tiene traducción, icono o ruta de navegación. Estás convirtiendo la finitud del tipo en una demostración por agotamiento ejecutable en el tiempo de una prueba unitaria, y esa es una de las pocas formas de verificación formal que un equipo adopta sin darse cuenta de que la está adoptando.

Componer: decodificación total

Un enum con tipo crudo obtiene Codable sintetizado gratis, codificándose como su valor crudo. El problema aparece cuando el servidor envía un valor que tu versión del cliente no conoce: la decodificación falla y, si el enum está dentro de una lista, arrastra consigo el objeto entero.

La solución es hacer explícita la tolerancia, decodificando el tipo crudo y colapsando lo desconocido en un caso previsto para ello.

enum Categoria: String, Codable, CaseIterable {
    case libro, disco, pelicula
    case desconocida = "__desconocida__"

    init(from decoder: any Decoder) throws {
        let crudo = try decoder.singleValueContainer().decode(String.self)
        self = Categoria(rawValue: crudo) ?? .desconocida
    }
}

Con esto la frontera queda sellada por ambos lados: nada entra sin validarse y nada desconocido derriba la decodificación. Y la prueba de ida y vuelta sobre allCases verifica el contrato completo en tres líneas.

for caso in Categoria.allCases {
    assert(Categoria(rawValue: caso.rawValue) == caso)
}

Raw values y CaseIterable resuelven, juntos, las dos mitades del mismo problema: el primero traduce entre el dominio cerrado y el mundo abierto, el segundo permite recorrer ese dominio cerrado por completo para demostrar que la traducción es correcta.

⚔️ Sella la frontera
  1. Explica por qué rawValue no es opcional y el inicializador sí, en términos de funciones totales y parciales.
  2. Define Moneda con tipo crudo String explícito y comenta por qué no dejas que el compilador lo infiera.
  3. Añade CaseIterable y escribe un test que compruebe la ida y vuelta para todos los casos.
  4. Implementa Categoria con caso desconocida y decodifica un JSON con un valor inventado sin que falle.
  5. Escribe un struct que conforme RawRepresentable sobre String con constantes estáticas, y razona cuándo es preferible a un enum cerrado.