Пошук уроків, статей та іншого контенту
Інтегруйте Zod із TypeScript для декларативних схем, виведення типів і типобезпечної перевірки форм.
TypeScript перевіряє типи під час компіляції, але не перевіряє дані під час виконання програми. Дані з форми, API, localStorage або URL-параметрів надходять у програму під час виконання, тому їхній тип не можна вважати безпечним автоматично.
const user = JSON.parse(responseText);
// TypeScript не знає, чи справді user має таку структуру
console.log(user.email);Zod дає змогу:
описати структуру даних декларативною схемою;
перевірити дані під час виконання;
отримати TypeScript-тип зі схеми;
використовувати той самий опис для форм, API та інших зовнішніх даних.
Основна ідея:
Схема Zod є джерелом правди для перевірки даних, а TypeScript-тип виводиться зі схеми.
Встановіть пакет:
npm install zodПісля цього імпортуйте z:
import { z } from "zod";z містить конструктори схем: z.string(), z.number(), z.object(), z.array() та інші.
Розглянемо схему реєстраційної форми:
import { z } from "zod";
const registrationSchema = z.object({
fullName: z
.string()
.trim()
.min(2, "Ім’я має містити щонайменше 2 символи"),
email: z
.string()
.trim()
.email("Введіть коректну адресу електронної пошти"),
age: z
.number()
.int("Вік має бути цілим числом")
.min(18, "Реєстрація доступна з 18 років")
.max(120, "Введіть коректний вік"),
});Схема описує об’єкт із трьома полями:
fullName — рядок без зайвих пробілів на початку та в кінці, мінімум із двома символами;
email — рядок у форматі електронної адреси;
age — ціле число від 18 до 120.
Методи перевірки можна ланцюжити. Кожен наступний метод додає нове правило.
z.inferТип не потрібно дублювати вручну:
type RegistrationData = z.infer<typeof registrationSchema>;TypeScript виведе такий тип:
type RegistrationData = {
fullName: string;
email: string;
age: number;
};Тепер цей тип можна використовувати в React-компонентах, функціях або сервісах:
const validData: RegistrationData = {
fullName: "Олена Коваль",
email: "olena@example.com",
age: 25,
};Якщо спробувати передати неправильне значення, TypeScript повідомить про помилку:
const invalidData: RegistrationData = {
fullName: "Олена Коваль",
email: "olena@example.com",
age: "25", // Помилка: очікується number
};Важливо розуміти різницю:
z.infer працює під час компіляції;
schema.parse() і schema.safeParse() перевіряють дані під час виконання.
parseМетод parse повертає перевірені дані, якщо вони відповідають схемі. Якщо дані некоректні, метод викидає помилку ZodError.
const data = registrationSchema.parse({
fullName: "Олена Коваль",
email: "olena@example.com",
age: 25,
});
console.log(data.fullName);parse зручно використовувати, коли помилка має оброблятися на рівні try...catch:
try {
const data = registrationSchema.parse({
fullName: "Олена",
email: "invalid-email",
age: 16,
});
console.log("Дані перевірено:", data);
} catch (error) {
console.error("Дані некоректні:", error);
}Для форм зазвичай зручніший safeParse, оскільки він не викидає виняток.
safeParsesafeParse повертає об’єкт із полем success:
const result = registrationSchema.safeParse({
fullName: "Олена Коваль",
email: "olena@example.com",
age: 25,
});
if (result.success) {
// Тут result.data має тип RegistrationData
console.log("Перевірені дані:", result.data);
} else {
// Тут result.error містить інформацію про помилки
console.log(result.error.issues);
}Якщо перевірка успішна:
result.success === trueто властивість result.data містить дані, які відповідають схемі.
Якщо перевірка неуспішна:
result.success === falseто доступна властивість result.error.
Кожна помилка має шлях до поля та повідомлення:
const result = registrationSchema.safeParse({
fullName: "",
email: "wrong",
age: 15,
});
if (!result.success) {
for (const issue of result.error.issues) {
console.log(issue.path, issue.message);
}
}Можливий результат:
[ 'fullName' ] Ім’я має містити щонайменше 2 символи
[ 'email' ] Введіть коректну адресу електронної пошти
[ 'age' ] Реєстрація доступна з 18 роківНижче наведено повний приклад React-компонента. Він:
зберігає значення полів;
перевіряє форму через Zod;
показує помилки біля відповідних полів;
передає далі лише перевірені дані.
import { FormEvent, useState } from "react";
import { z } from "zod";
const registrationSchema = z.object({
fullName: z
.string()
.trim()
.min(2, "Ім’я має містити щонайменше 2 символи"),
email: z
.string()
.trim()
.email("Введіть коректну адресу електронної пошти"),
age: z
.number()
.int("Вік має бути цілим числом")
.min(18, "Реєстрація доступна з 18 років")
.max(120, "Введіть коректний вік"),
});
type RegistrationData = z.infer<typeof registrationSchema>;
const initialForm: RegistrationData = {
fullName: "",
email: "",
age: 0,
};
export default function RegistrationForm() {
const [form, setForm] = useState<RegistrationData>(initialForm);
const [errors, setErrors] = useState<Record<string, string[]>>({});
const [submittedData, setSubmittedData] =
useState<RegistrationData | null>(null);
const updateField = (
field: keyof RegistrationData,
value: string
) => {
setForm((currentForm) => ({
...currentForm,
[field]: field === "age" ? Number(value) : value,
}));
setErrors((currentErrors) => ({
...currentErrors,
[field]: [],
}));
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const result = registrationSchema.safeParse(form);
if (!result.success) {
const nextErrors: Record<string, string[]> = {};
for (const issue of result.error.issues) {
const field = issue.path[0];
if (typeof field === "string") {
nextErrors[field] ??= [];
nextErrors[field].push(issue.message);
}
}
setErrors(nextErrors);
setSubmittedData(null);
return;
}
setErrors({});
setSubmittedData(result.data);
// result.data має тип RegistrationData і пройшов перевірку
console.log("Форму успішно надіслано:", result.data);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="fullName">Ім’я</label>
<input
id="fullName"
name="fullName"
value={form.fullName}
onChange={(event) =>
updateField("fullName", event.target.value)
}
/>
{errors.fullName?.map((message) => (
<p key={message}>{message}</p>
))}
</div>
<div>
<label htmlFor="email">Електронна пошта</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={(event) =>
updateField("email", event.target.value)
}
/>
{errors.email?.map((message) => (
<p key={message}>{message}</p>
))}
</div>
<div>
<label htmlFor="age">Вік</label>
<input
id="age"
name="age"
type="number"
value={form.age}
onChange={(event) => updateField("age", event.target.value)}
/>
{errors.age?.map((message) => (
<p key={message}>{message}</p>
))}
</div>
<button type="submit">Зареєструватися</button>
{submittedData && (
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
)}
</form>
);
}У цьому прикладі тип стану форми та схема узгоджені:
const [form, setForm] = useState<RegistrationData>(initialForm);Якщо змінити схему, TypeScript допоможе знайти місця, які потрібно оновити.
transformHTML-форми часто повертають рядки, навіть якщо поле має type="number". Для таких випадків схема може приймати рядок і перетворювати його на число:
const ageSchema = z
.string()
.trim()
.regex(/^\d+$/, "Введіть число")
.transform(Number)
.refine((age) => age >= 18, "Реєстрація доступна з 18 років");
const age = ageSchema.parse("25");
console.log(age); // 25Тут вхідний тип і результат відрізняються:
type AgeInput = z.input<typeof ageSchema>; // string
type AgeOutput = z.output<typeof ageSchema>; // numberz.infer<typeof schema> відповідає вихідному типу, тобто в цьому прикладі є еквівалентом z.output<typeof ageSchema>:
type Age = z.infer<typeof ageSchema>; // numberЦе корисно, коли форма працює з рядковими значеннями, а бізнес-логіка має отримати вже перетворені значення.
Наприклад:
const profileSchema = z.object({
name: z.string().trim().min(2, "Введіть ім’я"),
age: z
.string()
.trim()
.regex(/^\d+$/, "Вік має бути числом")
.transform(Number)
.refine((value) => value >= 18, "Мінімальний вік — 18 років"),
});
type ProfileInput = z.input<typeof profileSchema>;
type Profile = z.output<typeof profileSchema>;
const input: ProfileInput = {
name: "Олена",
age: "25",
};
const profile: Profile = profileSchema.parse(input);
console.log(profile.age); // numberТипізація змінної не перевіряє дані автоматично:
type ApiUser = {
id: number;
email: string;
};
const user = response as ApiUser;Оператор as лише повідомляє TypeScript, що розробник вважає дані коректними. Він не виконує перевірку під час виконання.
Для зовнішніх даних краще використовувати схему:
const apiUserSchema = z.object({
id: z.number().int().positive(),
email: z.string().email(),
});
type ApiUser = z.infer<typeof apiUserSchema>;
function readUser(payload: unknown): ApiUser | null {
const result = apiUserSchema.safeParse(payload);
if (!result.success) {
console.error("Некоректні дані користувача");
return null;
}
return result.data;
}Параметр має тип unknown, тому функція не може випадково використати неперевірені властивості. Об’єкт стає типом ApiUser лише після успішної перевірки.
Іноді потрібно перевірити тільки одне поле. Для цього можна використовувати метод pick:
const emailSchema = registrationSchema.pick({
email: true,
});
const result = emailSchema.safeParse({
email: "olena@example.com",
});Або перевірити окреме поле без створення нової схеми:
const result = registrationSchema.shape.email.safeParse(
"olena@example.com"
);
if (result.success) {
console.log("Email коректний:", result.data);
}Метод shape дає доступ до схем окремих полів об’єкта.
type FormData = {
email: string;
};Цей тип не перевіряє значення під час виконання. Рядок "not-an-email" все одно має тип string.
Для перевірки значень потрібна схема Zod:
const formSchema = z.object({
email: z.string().email(),
});as замість перевіркиconst data = input as RegistrationData;Це не валідація. Якщо input має неправильну структуру, помилка виникне пізніше, в іншому місці програми.
Використовуйте:
const result = registrationSchema.safeParse(input);Якщо схема використовує transform, її вхідний і вихідний типи можуть відрізнятися.
const schema = z.string().transform(Number);
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // numberДля значень форми використовуйте z.input, а для результату після перевірки — z.output або z.infer.
Не обов’язково окремо писати тип, який точно повторює схему:
const schema = z.object({
email: z.string().email(),
});
type Data = {
email: string;
};Такі дублікати можуть розійтися після зміни схеми. Краще вивести тип:
type Data = z.infer<typeof schema>;parse без обробки помилкиconst data = schema.parse(userInput);Якщо введення користувача може бути некоректним, необроблений виняток може перервати виконання. Для форм зазвичай краще:
const result = schema.safeParse(userInput);Zod виконує перевірку даних під час виконання.
TypeScript перевіряє використання типів під час компіляції.
z.object() описує структуру об’єкта декларативною схемою.
z.infer<typeof schema> виводить тип TypeScript зі схеми.
parse() повертає дані або викидає помилку.
safeParse() повертає об’єкт із success, тому зручний для форм.
z.input<typeof schema> описує вхідні дані схеми.
z.output<typeof schema> і z.infer<typeof schema> описують результат після перевірки та перетворень.
Після успішного safeParse програма може безпечно використовувати result.data.