Inferir tipos y forzar el tipo de entrada
Un schema que acepta o lanza una excepción alcanza para describir datos. Para construir algo con eso hacen falta dos cosas más: una forma de usar esa forma de datos en tu propio código, y una forma de manejar un fallo sin que se dispare una excepción.
Empecemos con un schema que describe a un profesor:
import * as z from 'zod'
const teacherSchema = z.object({
name: z.string(),
age: z.number(),
})Un schema, dos funciones
En TypeScript normalmente escribirías la forma de datos por segunda vez:
type Teacher = {
name: string
age: number
}Ahora la misma forma vive en dos lugares, y empiezan a desincronizarse la primera vez que alguien agrega un campo en uno solo de ellos.
z.infer toma el tipo directamente del schema:
type Teacher = z.infer<typeof teacherSchema>
// { name: string; age: number }Agrega una clave al schema y el tipo se actualiza solo. El schema es la definición, y TypeScript la lee.
typeof ahí adentro se ve raro porque no es el typeof de JavaScript que ya conoces. Esta es la versión de TypeScript, que pregunta "cuál es el tipo de este valor" a nivel de tipos. Puedes tratar toda la línea como una sola frase: dame el tipo que describe este schema.
Fallar sin excepciones
parse lanza una excepción. Eso funciona bien en un punto de entrada donde una solicitud mala debería detener todo, y no sirve para nada donde quieres examinar qué salió mal.
safeParse te devuelve un resultado en su lugar:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: 21 })
console.log(result)
// { success: true, data: { name: 'Jonathan', age: 21 } }Cuando falla, la forma cambia:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: '21' })
console.log(result.success)
// falseUn éxito te da success: true y data. Un fallo te da success: false y error. Así, un formulario puede hacer la pregunta y decidir qué hacer:
if (result.success) {
console.log('Se puede enviar sin problema:', result.data)
} else {
console.log('Muéstrale al usuario qué salió mal:', result.error.issues)
}parse lanza una excepción, que detiene todo a menos que la captures. safeParse devuelve un objeto con una marca success, así que puedes consultarla y seguir adelante.
Acotar lo que aceptas
Un tipo es un filtro grueso. z.number() acepta tanto -4 como 9e99, y ninguna de las dos es la edad de un profesor.
Los métodos se encadenan sobre un schema para hacerlo más estricto:
const teacherSchema = z.object({
name: z.string(),
age: z.number().min(18),
isAmerican: z.boolean().optional(),
id: z.number().default(() => Math.random()),
}).min(18)rechaza cualquier valor menor a dieciocho..gte()y.lte()cumplen la misma función con comparaciones explícitas..optional()permite que la clave falte por completo..default()provee un valor cuando la clave está ausente, así que el objeto parseado siempre tiene uno.
Zod también incluye validaciones para formatos comunes, así que un email es una sola llamada:
const contactSchema = z.object({
email: z.email(),
})
contactSchema.parse({ email: '[email protected]' }) // bien
contactSchema.parse({ email: 'jabbahuttcorp.com' }) // ZodErrorEso es lo que la librería quiere decir con declarativo: describes el resultado que quieres, y ella se encarga de la verificación.
Forzar el tipo de lo que llega
Los campos de un formulario HTML envían strings. Todos y cada uno, incluyendo el campo etiquetado "edad" con su selector numérico al lado.
Así que un schema que espera z.number() rechaza una entrada de formulario perfectamente válida, porque '13' es un string. La coerción convierte primero y valida después:
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
})
characterSchema.parse({ name: 'Luke Skywalker', episode: '4' })
// { name: 'Luke Skywalker', episode: 4 }Entró un string, salió un número, y cualquier verificación adicional sobre esa clave se aplicó contra el número.
Así que z.coerce.number().min(18) convierte el texto a número y después pregunta si ese número es al menos dieciocho.
Ponlo en práctica
Partiendo de este schema y este objeto:
const characterSchema = z.object({
name: z.string(),
episode: z.number(),
})
const character = {
name: 'Jabba the Hutt',
episode: '6',
}Haz cuatro cambios:
- Crea un tipo
Characterinferido del schema, y anota el objeto con él. - Agrega al schema una clave booleana opcional llamada
isJedi. - Haz que
episodeacepte el string'6'y lo guarde como número. - Registra en consola si la validación pasó o falló, como un solo booleano.
Compara tus respuestas
type Character = z.infer<typeof characterSchema>
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
isJedi: z.boolean().optional(),
})
const character: Character = {
name: 'Jabba the Hutt',
episode: '6',
}
console.log(characterSchema.safeParse(character).success)
// trueCuatro notas sobre los cuatro cambios:
z.infer<typeof characterSchema>toma el tipo directamente del schema, así que agregarisJedilo actualiza sin necesidad de una segunda edición..optional()significa que la clave puede estar ausente, por esocharacterno necesitaisJediy aun así pasa la validación.z.coerce.number()convierte antes de verificar, así que el string'6'se convierte en el número6..successes el booleano. Registrar solosafeParse(...)imprime todo el objeto de resultado, yparsete daría los datos o una excepción, y ninguno de los dos es un booleano.
Un detalle que vale la pena notar: con episode forzado por coerción, el tipo anotado dice number mientras que el objeto literal contiene '6'. Ahí es donde las formas de entrada y de salida difieren, y por eso importa leer result.data en lugar del objeto original.
Hacia dónde sigue esto
Un safeParse fallido devuelve un error, y hasta ahora eso ha sido algo cuya existencia comprobamos, más que algo que realmente leemos.
Leer errores de validación profundiza en eso: qué campo falló, qué tenía de malo, y cómo convertir eso en mensajes que una persona pueda usar.

