typescript - Hacer que zod realice verificación de tipo a nivel superior en la entrada de .parse()
Entiendo que el objetivo de zod es analizar datos de entrada no confiables y afirmar que son de un tipo que coincide con tu esquema.
Pero normalmente esos datos llegan a través de APIs web que garantizan al menos su forma de nivel superior, como string u object.
Parece que tendría sentido que Zod realizara esta verificación de tipo de nivel superior en parse(), aunque solo sea para prevenir errores tontos como los errores tipográficos. Pero, aparentemente, no lo hace.
Como un ejemplo simplificado para ilustrar
const schema = z.string().email();
schema.parse(1); // no type error here - why?
Parece que parse(1) debería tener un error de tipo en tiempo de compilación, porque sabemos que es imposible que el literal number 1 se valide correctamente en tiempo de ejecución. No podemos hacer eso con algún string aleatorio como entrada: se requiere el análisis en tiempo de ejecución para asegurarnos de que sea un correo electrónico válido, pero un number aquí parece un error obvio del programador y ni siquiera debería compilar.
Un ejemplo más práctico, que me llevó a hacer esta pregunta
async function validateRequest(request: Request) {
const someSchema = z.object({ ... })
return someSchema.parse(request.json()) // didn't await request.json() so won't work
}
Un error tonto como omitir el await arriba, parece que debería ser fácil de detectar mediante someSchema.parse() verificando que le paso un object, no un Promise<object>.
Entonces, ¿hay una manera de habilitar esta verificación de tipo de nivel superior con zod?
¿O es este comportamiento intencional por alguna razón que no he entendido sobre el diseño de Zod?
Respuesta #1
Intenté buscar una manera de hacer esto en Zod, pero no pude encontrar nada convincente. Sospecho que ese caso de uso puede no estar soportado de manera predeterminada. Una sugerencia, si quieres seguir haciendo tu análisis con Zod, sería crear una función envolvente con tipos más estrictos que internamente llame a tu esquema de Zod.
// I went for a record type because Promise<any> is assignable to object
function parseResponse(input: Record<string, unknown>) {
const someSchema = z.object({ ... });
return someSchema.parse(input)
}
async function validateRequest(request: Request) {
return parseResponse(request.json()) // This will throw because now it sees the promise
}
Editar :
De hecho, podrías escribir este tipo de ayudante en general así:
import { z } from "zod";
const schema = z.object({
field: z.string(),
})
function restrict<T, Output, Def extends z.ZodTypeDef, Input = Output>(schema: z.ZodType<Output, Def, Input>) {
return (t: T) => schema.parse(t);
}
type InferTypes<Z> = Z extends z.ZodType<infer Output, infer Defs, infer Input> ? [Output, Defs, Input] : [never, never, never];
type InferOutput<Z> = InferTypes<Z>[0];
type InferDefs<Z> = InferTypes<Z>[1];
type InferInput<Z> = InferTypes<Z>[2];
const validate = restrict<
Record<string, unknown>,
InferOutput<typeof schema>,
InferDefs<typeof schema>,
InferInput<typeof schema>
>(schema);
validate({}); // This typechecks because it's possible
async function foo(req: Request) {
return validate(req.json()); // This has a type error.
}
Con el único inconveniente real siendo la pila de inferencia de tipos que necesitas hacer para que los genéricos funcionen.
Biblioteca Alternativa
Si estás abierto a bibliotecas alternativas, esto es algo que se admite de manera relativamente de primera clase en io-ts. Puedes declarar explícitamente los tipos de entrada y salida de tus esquemas, aunque por defecto comienzan como unknown. La desventaja está en la experiencia del desarrollador. io-ts utiliza un paradigma funcional que requiere mucho tiempo para aprender al principio.
Respuesta #2
Es bastante fácil de hacer usando z.input, por ejemplo:
export const parse = <T extends z.ZodTypeAny, I extends z.input<T>>(
rawInput: I,
schema: T,
): z.output<T> => {
return schema.parse(rawInput);
};
