Галоўная / Артыкулы / Замена as-Casts на Zod Parsing ў кожным межах дадзеных Next.js

Замена as-Casts на Zod Parsing ў кожным межах дадзеных Next.js

Чаму кастыяванне TypeScript не можа захаваць вас ад зменыях API, і як шыматлік Zod паўтарае рэзультаты fetch, формы, працоўнікі маршрутаў і дзеянні сервера ў Next.js.

1102 слоў

Компаненты, створаныя за дапамою тэксту, здаюцца безпечнымі, пакуйлі-як толькі система выдае поле з іншым іменем, null заместо строкі, або пакет з адазой заместо даных корыстніка. TypeScript не можа гэта зафіксаваць: яго типы зникаюць пад час компілявання, а даныя з сеті існуюць толькі пад час выканання, таму as User ўособлівае тверджэнне, а не перакананне. У гідзе показана, як адна схема Zod можа як пераканаваць правільнасць прыходзячых даных, так і ствараць тип для TypeScript, а таксама як ўжываць яе на кожнай грані прыложэння React і Next.js: у результатах fetch, формах, обробчыках маршрутаў і дзеяннях сервера.

Ненадзірваны JSON — гэта справжняя проблема

Кожны пакет дадзеных, які ваш код не стварыў сам, чы то адпаведна fetch адпаведзь, тэла запытку, данні для адпаведных дзеянняў сервера чы webhook, заслуговае на падазрэнне. Якщо прыхіліцца да пераказу без перагляду пад час выканання, будуць абмовленыя прызначэнні типаў, верыфікаторы, якія не адпавядаюць інтэрфейсам, а таксама неспакоўныя типы кліента і сервера. Zod адносуе ўсё гэта да адной дыферэнціявы: як толькі зменяецца схема, зменяецца і выведзены тип.

Задаюце схему, выведзіце тип

Пачніце з імпорту:

import { z } from "zod";

Наведзеная нижэй схема описвае прафіль пользователя, z.infer ператварае яго ў тип TypeScript, а loadProfile прабывае адпаведна parse рэспанс, прычым яго і вяртае.

export const UserProfileSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  displayName: z.string().min(1).optional(),
});export type UserProfile = z.infer<typeof UserProfileSchema>;async function loadProfile(id: string): Promise<UserProfile> {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  return UserProfileSchema.parse(data);
}

Пораўняйце return data as UserProfile: парсінг выклікае адхылення ў той момент, калі API не выпанавае узгаданыя правіла, тады як прызначэнне типа дазволяе некоректным дадзенням працаваць, пакуль не станеться крах у далёкай частцы системы.

У коде UI зазвычай краща варыянт safeParse: ён вяртае об’ект з рэзультатамі замест таго, каб выклікаць адказку, таму вы самі контролюеце альтэрнатыўны варыянт:

const result = UserProfileSchema.safeParse(data);
if (!result.success) {
  console.error(result.error.flatten());
  return null;
}

Формы, якія прасылаюць валідныя даныя да функцыі обробкі

За дапамогою zodResolver React Hook Form пераканальвае значэнні прытаму, як толькі яны доходзяць да handleSubmit. Файл ўтварае кліентскі компонент:

"use client";

Паведамленні пра паказкі полей таксама выходзяць з схемы, што дапамагае падтрымваць супараднасць між інтерфейсамі і типамі:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";const SignupSchema = z.object({
  email: z.string().email("Enter a valid email"),
  password: z.string().min(8, "At least 8 characters"),
});type SignupValues = z.infer<typeof SignupSchema>;export function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignupValues>({
    resolver: zodResolver(SignupSchema),
  });  return (
    <form onSubmit={handleSubmit((values) => console.log(values))}>
      <input type="email" {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register("password")} />
      {errors.password && <p>{errors.password.message}</p>}
      <button type="submit">Create account</button>
    </form>
  );
}

У рэальным прыкладзе аплікацыі краща перанесці SignupSchema у спяльны модуль замест таго, каб яго визначаць унутры файлу компонента, тады сервер зможа імпортаваць тыя ж правіла.

Пераканальванне дадзеных у точках запуску Next.js

Обробнікі маршрутаў

Для обробніка маршрута ўпэўненасць у валіднасці дадзеных патрабуе NextResponse, бібліятэку Zod і спяльную схему профіля:

import { NextResponse } from "next/server";
import { z } from "zod";
import { UserProfileSchema } from "@/lib/schemas/user";

Адміністратар перавершае тэлы з дапамогай safeParse і, у разе нявернення успеху, вяртае код 400 з адказамі працоўкі. Ён таксама перавершае свой сабэтны адказ па шыму UserProfileSchema, тады выходны данні супараднаюцься з умовамі кліента. Жорсткая закодаваная значэнне id выступае ў ролі ідэнтыфікатора для запісу ў базе дадзеных.

const CreateUserSchema = z.object({
  email: z.string().email(),
  displayName: z.string().min(1).max(80).optional(),
});export async function POST(request: Request) {
  const parsed = CreateUserSchema.safeParse(await request.json());
  if (!parsed.success) {
    return NextResponse.json(
      { error: "Invalid body", details: parsed.error.flatten() },
      { status: 400 }
    );
  }  const created = {
    id: "11111111-1111-1111-1111-111111111111",
    email: parsed.data.email,
    displayName: parsed.data.displayName,
  };  return NextResponse.json(UserProfileSchema.parse(created), { status: 201 });
}

Дзеянні сервера

Модуль дзеяння сервера пачынаецца з дырэктывы:

"use server";

Дзеянне стварае об’ект з FormData і перавершае яго па там самым шыму SignupSchema, які викорыстоўваецца у форме. Вярненне значэння ok у виглядзе літэральнага типу (as const) дазволяе тым, хто вызывае, чыста адсортаваць рэзультаты:

import { SignupSchema } from "@/lib/schemas/auth";export async function signupAction(formData: FormData) {
  const parsed = SignupSchema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });  if (!parsed.success) {
    return { ok: false as const, errors: parsed.error.flatten().fieldErrors };
  }  return { ok: true as const };
}

Адзін модуль схемы, які выкарыстоўваецца як кліентам, так і серверам, пазбавляе нас проблемы «валідна ў форме, але адхіленая серверам». Па аднаковым падходу за межами Next.js дывіцеся як выкарыстоўваць адну схему Zod ў React-фронтэнде і Node-бэкендзе.

Звычкі, якія дапамагаюць падтрымваць схемы

  • Зберагаюце схемы разам, напрыклад пад lib/schemas/*.
  • Ствараюце варіянты схемы за дапамою .extend, .pick і .omit, а не дублюючы поля.
  • Выкарыстоўваеце .transform толькі для простых задач, такіх як адсечка страк чы паспрацоўка дат, а не для скрытых бізнес-правіл.
  • Выкарыстоўваеце z.discriminatedUnion, калі форма пакета данных залежыць ад поля статусу.
  • Паспрацоўвайце зменныя сераўіса раз адну, пад час запуску.

У практыцы складанне выглядае так. Базавае схема мае спільныя поля:

const BaseUser = z.object({
  email: z.string().email(),
  displayName: z.string().optional(),
});

На ўзмацненне схемы кожнае поле стае необавязковым за дапамою .partial(), а DTO з рэспансам выконвае дадачы поля, якія належаць серверу, за дапамою .extend():

export const UpdateUserSchema = BaseUser.partial();
export const UserDtoSchema = BaseUser.extend({
  id: z.string().uuid(),
  createdAt: z.string().datetime(),
});

Є адна прытаманнасць: новейшыя версіі Zod адкрываюць форматы вышэйшага рангу, такія як z.email() і z.uuid(), і змінююць спосаб прызначэння падазлов. Показаны тут ланцоўкі форматавання можаць стаць застарэлымі у вашай версіі, таму пераканаўцеся з ныякімі документамі Zod.

Ключовыя выводы

  • Тыпы описваюць намер; толькі парсаванне пад час выконання прыменяе яго на роўні сеті.
  • Выводзіце типы TypeScript з схем Zod, каб яны не адстаўалі адзін ад другога.
  • Выбірайце safeParse, калі хочаце кераваць падазламі, і parse, калі падазлы должны выкликаць аберанне.
  • Іспользуйце адно шыматлэнне для формы, працоўніка і дзеяння.
  • Выберыце самы небяспечны тип перакладу as і спачатку задаце для яго шыматлэнне.