Mini-Proyecto: Desarrollar una Librería de Validación Tipada

Video
30 min~5 min lectura

Reproductor de video

Concepto clave

En arquitecturas complejas, la validación de datos es una capa crítica que garantiza la integridad del sistema. Una librería de validación tipada en TypeScript va más allá de simples comprobaciones: utiliza tipos genéricos para inferir la estructura de los datos validados, creando un flujo de tipos seguro desde la entrada hasta el uso interno. Imagina un control de aduanas digital: no solo revisa pasaportes (validación), sino que también actualiza automáticamente los permisos del viajero (tipado) en el sistema.

El núcleo de este patrón es la composición de validadores. En lugar de funciones aisladas, construyes validadores reutilizables que se combinan como piezas de Lego, donde cada pieza aporta su lógica de validación y su contrato de tipos. Esto permite crear esquemas complejos para objetos anidados, arrays condicionales o uniones discriminadas, manteniendo el tipado preciso en cada paso.

"La magia no está en validar, sino en que TypeScript sepa exactamente qué forma tiene el dato después de pasar la validación."

Cómo funciona en la práctica

Vamos a construir la base de nuestra librería. Empezamos definiendo un tipo genérico para un validador:

type Validator = (input: unknown) => input is T;

Este tipo dice: "dame algo desconocido y te digo si es de tipo T". Pero queremos más: queremos transformar. Creamos un validador que también refine el tipo:

function string(): Validator {
    return (input: unknown): input is string => typeof input === 'string';
}

Ahora, la clave: combinadores. Un combinador toma validadores y devuelve uno nuevo:

function object<T extends Record<string, Validator<any>>>(schema: T): Validator<{ [K in keyof T]: InferType<T[K]> }> {
    return (input: unknown): input is any => {
        if (typeof input !== 'object' || input === null) return false;
        for (const key in schema) {
            if (!schema[key](input[key])) return false;
        }
        return true;
    };
}

Nota cómo InferType (que definiremos) extrae el tipo del validador. Si usamos object({ name: string(), age: number() }), TypeScript inferirá que el resultado es { name: string, age: number }.

Caso de estudio

Implementemos un validador para un formulario de usuario en una aplicación empresarial. Necesitamos validar:

  • Nombre: string, mínimo 2 caracteres
  • Email: string con formato de email
  • Edad: number opcional entre 18 y 100
  • Roles: array de strings, al menos uno seleccionado

Primero, creamos validadores básicos con tipos:

const minLength = (length: number): Validator => 
    (input): input is string => typeof input === 'string' && input.length >= length;

const email = (): Validator => 
    (input): input is string => typeof input === 'string' && /^[^@]+@[^@]+\.[^@]+$/.test(input);

const optional = (validator: Validator): Validator => 
    (input): input is T | undefined => input === undefined || validator(input);

Ahora, componemos el esquema completo:

const userSchema = object({
    name: compose(string(), minLength(2)),
    email: compose(string(), email()),
    age: optional(compose(number(), (n): n is number => n >= 18 && n <= 100)),
    roles: array(string())
});

Al validar const data = { name: "Ana", email: "[email protected]", roles: ["admin"] };, si pasa, TypeScript sabe que data es { name: string, email: string, age?: number, roles: string[] } con age opcional.

Errores comunes

  1. No inferir tipos correctamente en combinadores: Si tu función object devuelve Validator<any>, pierdes todo el tipado. Usa tipos genéricos anidados y infer para extraer tipos de validadores.
  2. Validar en tiempo de ejecución pero no en compilación: Asegúrate de que los esquemas sean tipos, no solo valores. Por ejemplo, schema: Record<string, Validator<any>> permite cualquier validador, pero T extends Record<string, Validator<any>> conserva las claves.
  3. Olvidar casos límite: ¿Qué pasa con null, undefined o arrays vacíos? Define políticas claras (ej., optional para opcionales) y documenta el comportamiento.
  4. No proporcionar mensajes de error útiles: En nivel avanzado, agrega un sistema de errores tipado que capture qué validador falló y por qué, sin romper la inferencia de tipos.
  5. Abusar de as en implementaciones: Evita afirmaciones de tipo (as) dentro de validadores; usa guardias de tipo (input is T) para mantener la seguridad.

Checklist de dominio

  • Puedo crear un validador genérico que infiera el tipo de salida correctamente.
  • Sé implementar combinadores como object, array, union que preserven el tipado.
  • He construido un esquema para un objeto anidado de 3 niveles con validaciones condicionales.
  • Puedo explicar la diferencia entre Validator<T> y una función que devuelve boolean.
  • He integrado mensajes de error tipados en mi librería sin perder inferencia.
  • Sé cómo testear validadores tanto en tipo como en ejecución.
  • Puedo optimizar la librería para evitar checks redundantes en objetos grandes.

Implementa una librería de validación tipada para esquemas de API REST

En este ejercicio, construirás una librería de validación que pueda manejar respuestas de API REST típicas, con tipos seguros. Sigue estos pasos:

  1. Define los tipos base: Crea un tipo Validator que sea una guardia de tipo. Implementa InferType que extraiga el tipo U de Validator.
  2. Implementa validadores primitivos: Escribe funciones para string(), number(), boolean(), y literal(value) (ej., literal('success')). Asegúrate de que devuelvan Validator.
  3. Crea combinadores: Implementa object(schema) para objetos, array(validator) para arrays, y optional(validator) para campos opcionales. Usa genéricos para inferir los tipos resultantes.
  4. Añade validaciones complejas: Agrega union(validators) para uniones (ej., string | number) y intersection(validators) para intersecciones. Prueba con un esquema que incluya ambos.
  5. Construye un esquema real: Valida una respuesta de API como { status: 'ok', data: { users: Array<{ id: number, name: string }> }, error?: string }. Escribe tests en TypeScript que verifiquen el tipado y la validación.

Entrega el código en un repositorio o archivo, con comentarios que expliquen las decisiones de diseño.

Pistas
  • Usa tipos condicionales como `T extends Validator ? U : never` para InferType.
  • En `object`, itera sobre las claves del esquema con un bucle `for...in` y aplica cada validador.
  • Para `union`, prueba cada validador en orden hasta que uno tenga éxito; asegura que el tipo inferido sea la unión de los tipos.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.