Заставить zod выполнять проверку типа верхнего уровня на входе .parse()
Я понимаю, что цель zod - проанализировать ненадежные входные данные и подтвердить, что они имеют тип, соответствующий вашей схеме.
Но обычно эти данные поступают через веб-API, которые гарантируют, по крайней мере, их форму верхнего уровня, например или.
Похоже, что zod имеет смысл выполнять эту проверку типов верхнего уровня на
parse()
, хотя бы для предотвращения таких глупых ошибок, как опечатки. Но, похоже, это не так.
В качестве упрощенного примера для иллюстрации
const schema = z.string().email();
schema.parse(1); // no type error here - why?
Это выглядит как
parse(1)
должна иметь ошибку типа времени компиляции, потому что мы знаем, что невозможно, чтобы буквальный
number
1 будет правильно проверяться во время выполнения. Мы не можем сделать это с каким-то случайным
string
input - требуется анализ времени выполнения, чтобы убедиться, что это действительный адрес электронной почты, - но число здесь кажется очевидной ошибкой программиста и не должно даже компилироваться.
Более практический пример, который заставил меня задать этот вопрос
async function validateRequest(request: Request) {
const someSchema = z.object({ ... })
return someSchema.parse(request.json()) // didn't await request.json() so won't work
}
Глупая ошибка, такая как пропуск
await
выше, кажется, что это должно быть легко поймать
someSchema.parse()
проверяя, что я прохожу
object
, а не
Promise<object>
.
Итак, есть ли способ включить эту проверку типов верхнего уровня с помощью zod?
Или это поведение намеренно по какой-то причине, которую я не понял в дизайне зода?
1 ответ
Я пытался найти способ сделать это в Zod, но не нашел ничего убедительного. Я подозреваю, что вариант использования может не поддерживаться из коробки. Одним из предложений, если вы хотите продолжить синтаксический анализ с помощью Zod, будет создание функции-оболочки с более строгими типами, которая внутренне вызывает вашу схему 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
}
Редактировать:
На самом деле вы могли бы написать такого рода помощника в целом следующим образом:
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.
}
Единственным реальным недостатком является стек вывода типа, который вам нужно сделать, чтобы заставить работать дженерики.
Альтернативная библиотека
Если вы открыты для альтернативных библиотек, это то, что относительно первоклассно поддерживается в io-ts. Вы можете явно указать типы ввода и вывода ваших схем, они просто начинаются как