IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar30/36-dars23 daqiqa
Mundarija (35)

Zod formalar uchun: coerce, refine, superRefine va transform

Qisqacha: Formadan keladigan har qiymat — satr, bo'sh maydon — "" (undefined emas). Zod 4 da z.coerce.number() satrni songa aylantiradi, lekin "" ni 0 qiladi — buni z.preprocess bilan to'sing. Bir nechta maydonni birga tekshirish — .refine(fn, { path }), bir nechta xato — .superRefine. .transform qiymatni o'zgartiradi, shuning uchun sxemaning kirish va chiqish turi farq qiladi (z.input / z.output). Xabarlarni har qoidaning o'zida o'zbekcha yozing.

Bu darsda

  • Formadan keladigan qiymatlar Zod'ga qanday tushishini va coerce ning "" → 0 tuzog'ini bilasiz.
  • Ixtiyoriy maydonni (optional, nullable, default) bo'sh satr bilan to'g'ri ishlatasiz.
  • refine bilan ikki maydonni solishtirasiz, xatoni kerakli maydonga (path) qo'yasiz; superRefine bilan bir nechta xato chiqarasiz.
  • transform qiymatni o'zgartirganda z.input va z.output farqini tushunasiz.
  • Xabarlarni o'zbekchalaysiz va vazifalar tahrir formasining sxemasini (zod/mini) o'qiysiz.

Oldin bilishingiz kerak: Zod va turni sxemadan chiqarish, API qatlami va type-safe kontrakt, React Hook Form I.

1. Nega bu kerak?

Oldingi darsda bron formasining qoidalarini register ichida yozdik: required, min, pattern. Ishladi. Lekin Jasur aka uch narsani so'radi:

  1. "Ketish vaqti kelishdan keyin bo'lsin." Bu — ikki maydonni birga tekshirish. register qoidasi faqat o'z maydonini ko'radi.
  2. "Telefon serverga faqat raqamlar bo'lib borsin: 998900000000." Mehmon esa +998 90 000 00 00 deb yozadi. Qiymatni tekshirish bilan birga o'zgartirish kerak.
  3. "Server ham xuddi shu qoidalarni tekshirsin." JSX ichidagi qoidalarni serverga olib bo'lmaydi.

Bularning uchalasini bitta joy hal qiladi — sxema. Zod'ni 15-qismda o'rgangansiz: z.object, z.infer, parse va safeParse. API qatlami darsida esa vazifalar server javobini zod/mini bilan tekshira boshladi. Bugun Zod'ni formalar tomonidan ko'ramiz. Formaning o'ziga xos muammolari bor: hamma narsa satr, bo'sh maydon "", bitta xato bir nechta maydonga tegishli.

Hayotiy o'xshatish: bojxona. Yo'lovchi (forma qiymati) chegaradan o'tishi uchun uch narsa bo'ladi. Hujjat tekshiriladi (tur va qoida). Pul so'mga almashtiriladi (transform). Ba'zan ikki hujjat bir-biriga solishtiriladi: chipta va pasport bir odamnikimi (refine). Sxema — shu bojxonaning qoidalar kitobi, va u chegaraning ikkala tomonida — brauzerda ham, serverda ham — bir xil.

Maslahat: Bu darsdagi Zod misollari saytda «Ishga tushir» bilan ishlaydi (natija oynasida zod 4.6.5 bor). Natija oynasi xabarlarni faqat inglizcha beradi — o'zbekcha matnni sxemada o'zimiz yozamiz. Konsol ko'rinishi — natija panelidagi kabi.

2. Formadan nima keladi?

2.1 Hamma narsa — satr

Brauzer maydon qiymatini doim satr qilib beradi. <input type="number"> da ham. RHF valueAsNumber siz, FormData esa doim shunday (Formalar II):

Maydon Mehmon nima qildi Qiymat
type="text" hech narsa yozmadi ""
type="number" 4 yozdi "4"
type="date" sanani tanladi "2026-10-14"
<select> "Manti" ni tanladi "manti"
checkbox (RHF) belgiladi true
checkbox (FormData) belgiladi / belgilamadi "on" / kalit yo'q

z.number() satrni qabul qilmaydi:

jsx
import { z } from "zod";

const Guests = z.number().int().min(1);
const result = Guests.safeParse("4");
console.log(result.success);
console.log(result.error.issues[0].message);

Konsolda:

text
false
Invalid input: expected number, received string

Tarjimasi: "Noto'g'ri kirish: son kutilgan edi, satr keldi". Zod haq — "4" haqiqatan ham satr.

2.2 z.coerce — majburan aylantirish

z.coerce.number() (coerce — "majburlash") tekshirishdan oldin qiymatni Number(…) dan o'tkazadi. Satr songa aylanadi, keyin odatdagi qoidalar ishlaydi:

jsx
import { z } from "zod";

const Num = z.coerce.number();
console.log(Num.parse("4"), Num.parse(" 12 "), Num.parse("1e3"));
console.log(Num.parse(""));
const bad = Num.safeParse("to'rt");
console.log(bad.error.issues[0].message);

Konsolda:

text
4 12 1000
0
Invalid input: expected number, received NaN

Uchta kutilmagan narsa bor. " 12 " — bo'shliqlar jim kesildi. "1e3" — ilmiy yozuv, 1000. Eng xavflisi — ikkinchi qator: bo'sh satr 0 ga aylandi. Number("") — 0 (turlarni aylantirish). Mehmon "Mehmonlar soni" ni bo'sh qoldirdi, sxema esa "0 kishi" deb qabul qildi. .min(1) bo'lsa, xabar "Too small: expected number to be >=1" bo'ladi — "maydonni to'ldiring" emas.

Zod 4 da coerce lar: z.coerce.number(), z.coerce.string(), z.coerce.boolean(), z.coerce.bigint(), z.coerce.date(). z.coerce.boolean() dan ehtiyot bo'ling: u Boolean(…) ni ishlatadi, "false" satri ham true bo'ladi (bo'sh bo'lmagan satr).

2.3 Bo'sh maydonni undefined ga aylantirish

Forma uchun to'g'ri yo'l: avval "" ni "qiymat yo'q" (undefined) ga aylantiramiz, keyin son qilamiz. Buni z.preprocess qiladi — "tekshiruvdan oldin ishlov":

jsx
import { z } from "zod";

const emptyToUndefined = (v) => (v === "" ? undefined : v);

const Guests = z.preprocess(
  emptyToUndefined,
  z.coerce
    .number({ error: "Nechta mehmon?" })
    .int("Butun son yozing")
    .min(1, "Kamida 1 kishi")
    .max(12, "Ko'pi bilan 12 kishi"),
);

for (const input of ["", "4", "2.5", "15"]) {
  const r = Guests.safeParse(input);
  const shown = r.success ? r.data : r.error.issues[0].message;
  console.log(input, "→", shown);
}

Konsolda:

text
 → Nechta mehmon?
4 → 4
2.5 → Butun son yozing
15 → Ko'pi bilan 12 kishi

Birinchi qatorda input bo'sh satr, shuning uchun qator → bilan boshlanadi. { error: "Nechta mehmon?" } — tur xatosi uchun xabar: qiymat yo'q (undefined) yoki son emas. Qolgan har qoidaga ham o'z xabari berildi.

Tekshirib ko'ring: z.preprocess siz, faqat z.coerce.number().min(1, "Kamida 1 kishi") bo'lsa, bo'sh maydon qanday xabar beradi?

Javob

"Kamida 1 kishi". "" → 0 bo'ladi, 0 < 1 — min xatosi. Xabar texnik jihatdan to'g'ri, lekin mehmon uchun chalkash: u "0 kishi" deb yozmagan, hech narsa yozmagan. preprocess bo'lsa — "Nechta mehmon?", ya'ni "to'ldiring" ma'nosidagi xabar. Agar .min umuman bo'lmasa, coerce yolg'iz "" ni jimgina 0 qilib o'tkazib yuborardi — eng yomon holat.

3. Ixtiyoriy maydonlar

3.1 optional, nullable, default

Bron formasida email ixtiyoriy: yozsa — chek yuboramiz, yozmasa — yo'q. Zod va turni sxemadan chiqarish darsidagi qurilish bloklarini eslaymiz:

Zod Qabul qiladi Chiqish turi
S.optional() undefined (yoki kalit yo'q) T | undefined
S.nullable() null T | null
S.nullish() undefined va null T | null | undefined
S.default(x) undefined → x bilan almashtiriladi T

Muammo: forma undefined emas, "" yuboradi. z.email().optional() esa "" ni email deb tekshiradi:

jsx
import { z } from "zod";

const Email = z.email("Email noto'g'ri").optional();
console.log(Email.safeParse(undefined).success);
console.log(Email.safeParse("").error.issues[0].message);

const EmailOrEmpty = z.union([
  z.literal(""),
  z.email("Email noto'g'ri"),
]);
console.log(EmailOrEmpty.safeParse("").success);
console.log(EmailOrEmpty.safeParse("ali@example.com").success);
console.log(EmailOrEmpty.safeParse("ali@").error.issues[0].message);

Konsolda:

text
true
Email noto'g'ri
true
true
Email noto'g'ri

z.email() — Zod 4 dagi yangi yozuv. Eski kodda z.string().email() ni ko'rasiz — u hali ishlaydi, lekin "eskirgan" (deprecated) deb belgilangan. z.url(), z.uuid(), z.iso.date() ham shunday — yuqori darajadagi funksiyalar.

Ikki yechim bor. z.union([z.literal(""), …]) — "yo bo'sh, yo to'g'ri email". Natija "" bo'lib qoladi. Yoki «Bo'sh maydonni undefined ga aylantirish» bo'limidagi preprocess — natija undefined bo'ladi. Serverga "email yo'q" ni undefined (yoki null) qilib yuborish odatda tozaroq.

Diqqat: .optional() formadagi bo'sh matn maydonini "ixtiyoriy" qilmaydi. z.string().optional() "" ni qabul qiladi, chunki "" — satr. z.string().min(1).optional() esa "" ni rad etadi. "Bo'sh bo'lsa ham bo'ladi" degan qoidani "" bilan sinab ko'ring.

3.2 Checkbox: "Shartlarga roziman"

Majburiy belgi uchun z.boolean() yetmaydi — false ham boolean. z.literal(true) — faqat true:

jsx
import { z } from "zod";

const Agree = z.literal(true, { error: "Qoidalarga rozilik kerak" });
console.log(Agree.safeParse(true).success);
console.log(Agree.safeParse(false).error.issues[0].message);

Konsolda:

text
true
Qoidalarga rozilik kerak

4. transform — qiymatni o'zgartirish

4.1 Telefon: tekshir, keyin raqamlarga aylantir

.transform(fn) — tekshiruvdan keyin qiymatni o'zgartiradi. Telefon mehmon uchun chiroyli ko'rinishda yoziladi, server esa raqamlarni oladi:

jsx
import { z } from "zod";

const PHONE = /^\+998 \d{2} \d{3} \d{2} \d{2}$/;

const Phone = z
  .string()
  .trim()
  .regex(PHONE, "Namuna: +998 90 000 00 00")
  .transform((s) => s.replace(/\D/g, ""));

console.log(Phone.parse(" +998 90 000 00 00 "));
console.log(Phone.safeParse("90 000 00 00").error.issues[0].message);

Konsolda:

text
998900000000
Namuna: +998 90 000 00 00

Tartib muhim: trim → regex → transform. Zanjir chapdan o'ngga ishlaydi. regex chiroyli ko'rinishni tekshiradi, transform esa undan keyin bo'shliq va + ni olib tashlaydi. \D — "raqam bo'lmagan har belgi" (muntazam ifodalar).

.trim() ham o'zgartiradi, lekin u turni o'zgartirmaydi: satr kirdi — satr chiqdi. Zod 4 da bunday qoidalar (trim, toLowerCase) "overwrite" — "ustidan yozish" deyiladi. .transform esa istalgan turga aylantira oladi.

4.2 Teglar: satrdan massivga

Mehmon taomlarni vergul bilan yozadi: osh, manti, ,lag'mon. Bizga massiv kerak:

jsx
import { z } from "zod";

const Dishes = z
  .string()
  .transform((s) => s.split(",").map((t) => t.trim()).filter(Boolean))
  .refine((list) => list.length > 0, "Kamida bitta taom yozing");

console.log(Dishes.parse("osh, manti, ,lag'mon"));
console.log(Dishes.safeParse(" , ").error.issues[0].message);

Konsolda:

text
(3) ['osh', 'manti', 'lag'mon']
Kamida bitta taom yozing

.filter(Boolean) — bo'sh bo'laklarni tashlaydi (filter). .refine transform'dan keyin turibdi — u allaqachon massivni ko'radi. Qoida: transform'dan keyingi har tekshiruv o'zgargan qiymatni oladi.

4.3 Ikki tur: z.input va z.output

Endi Dishes ning ikki "yuzi" bor: kirishda satr, chiqishda massiv. TypeScript buni ajratadi (bu blok — TS tekshiruvi bilan):

ts
import { z } from "zod";

const Dishes = z.string().transform((s) => s.split(","));
type DishesIn = z.input<typeof Dishes>;
type DishesOut = z.output<typeof Dishes>;

const typed: DishesIn = "osh, manti";
const parsed: DishesOut = "osh, manti";

Xato:

text
TS2322: Type 'string' is not assignable to type 'string[]'.

z.input — string (forma shuni beradi), z.output (= z.infer) — string[] (ilova shuni ishlatadi). typed qatori xatosiz, parsed esa xato: chiqish turi massiv. Bu farq kelgusi darsda muhim bo'ladi: forma kirish turi bilan to'ldiriladi, yuborish funksiyasi esa chiqish turini oladi.

z.input z.output
Kim ishlatadi forma maydonlari, defaultValues onSubmit, server so'rovi
Phone string ("+998 90 …") string ("998900000000")
Dishes string string[]
z.coerce.number() unknown number

Tekshirib ko'ring: z.string().transform((s) => s.length).refine((n) => n > 2) — refine nimani oladi va z.output qanday tur?

Javob

refine sonni oladi — transform natijasini (satr uzunligini). z.output — number, z.input — string. Zanjir chapdan o'ngga: avval satr tekshiriladi, keyin uzunlikka aylanadi, keyin uzunlik tekshiriladi.

5. refine va superRefine

5.1 Ikki maydonni solishtirish

Jasur akaning birinchi talabi: "ketish kelishdan keyin". .refine ni obyekt sxemasiga qo'yamiz — u butun obyektni oladi. path — xato qaysi maydon ostida chiqishi:

jsx
import { z } from "zod";

const Booking = z
  .object({
    name: z.string().trim().min(2, "Ism kamida 2 harf"),
    from: z.string(),
    to: z.string(),
  })
  .refine((b) => b.to > b.from, {
    error: "Ketish kelishdan keyin bo'lsin",
    path: ["to"],
  });

const input = { name: "A", from: "19:00", to: "18:00" };
const r = Booking.safeParse(input);
console.log(r.error.issues.map((i) => `${i.path}: ${i.message}`));
console.log(z.flattenError(r.error).fieldErrors);

Konsolda:

text
(2) ['name: Ism kamida 2 harf', "to: Ketish kelishdan keyin bo'lsin"]
{name: ['Ism kamida 2 harf'], to: ["Ketish kelishdan keyin bo'lsin"]}

"18:00" > "19:00" — satrlarni solishtirish, "HH:MM" ko'rinishida u vaqt tartibiga mos (satrlarni solishtirish). path: ["to"] bo'lmasa, xato obyektning o'ziga tegishli bo'lardi (path bo'sh) va forma uni hech bir maydon ostiga qo'ya olmasdi.

z.flattenError — xatolarni forma uchun qulay ko'rinishga keltiradi: fieldErrors — maydon → xabarlar massivi, formErrors — hech qaysi maydonga tegishli bo'lmaganlari. Kelgusi darsdagi resolver ham xatolarni shu tamoyil bilan errors.to.message ga joylaydi.

error: — Zod 4 dagi nom. Eski kodda message: ni ko'rasiz — u ham ishlaydi, lekin yangi kodda error yozing.

5.2 Tuzoq: tur xatosi bo'lsa, refine ishlamaydi

Yuqoridagi misolda name xato bo'lsa ham refine ishladi — ikkala xabar chiqdi. Endi guests maydonini qo'shamiz va uni yubormaymiz:

jsx
import { z } from "zod";

const Booking = z
  .object({ from: z.string(), to: z.string(), guests: z.number() })
  .refine((b) => b.to > b.from, {
    error: "Ketish kelishdan keyin bo'lsin",
    path: ["to"],
  });

const r = Booking.safeParse({ from: "19:00", to: "18:00" });
console.log(r.error.issues.map((i) => i.message));

Konsolda:

text
(1) ['Invalid input: expected number, received undefined']

Vaqt xatosi chiqmadi. Zod 4 qoidasi: obyektda tur xatosi (maydon yo'q, son o'rniga satr) bo'lsa, obyekt darajasidagi refine ishga tushmaydi — chunki u b.guests ni son deb ishonib yozilgan. min kabi qoida xatolari esa to'sqinlik qilmaydi.

Formada bu shunday ko'rinadi: mehmon vaqtni noto'g'ri tanladi, lekin xabar faqat boshqa maydon to'g'rilangandan keyin chiqadi. Kerak bo'lsa, when parametri bilan "qachon ishlasin" ni o'zingiz aytasiz:

jsx
import { z } from "zod";

const Booking = z
  .object({ from: z.string(), to: z.string(), guests: z.number() })
  .refine((b) => b.to > b.from, {
    error: "Ketish kelishdan keyin bo'lsin",
    path: ["to"],
    when: (payload) =>
      payload.issues.every(
        (i) => i.path[0] !== "from" && i.path[0] !== "to",
      ),
  });

const r = Booking.safeParse({ from: "19:00", to: "18:00" });
console.log(r.error.issues.map((i) => i.message));

Konsolda:

text
(2) ['Invalid input: expected number, received undefined', "Ketish kelishdan keyin bo'lsin"]

when — "from va to da xato yo'q bo'lsa, ishlayver". Endi ikkala xabar birga.

5.3 superRefine — bir nechta xato

refine bitta xabar beradi. Buyurtmada bir taom ikki marta yozilgan bo'lsa-chi? Har takror qatorga alohida xato kerak. superRefine — ctx (kontekst) oladi va xohlagancha xato qo'shadi:

jsx
import { z } from "zod";

const Order = z
  .object({ dishes: z.array(z.string().trim().min(1)) })
  .superRefine((order, ctx) => {
    order.dishes.forEach((dish, i) => {
      if (order.dishes.indexOf(dish) !== i) {
        ctx.issues.push({
          code: "custom",
          input: dish,
          path: ["dishes", i],
          message: `«${dish}» yuqorida bor`,
        });
      }
    });
  });

const dishes = ["osh", "manti", "osh", "manti"];
const r = Order.safeParse({ dishes });
const show = (i) => `${i.path.join(".")}: ${i.message}`;
console.log(r.error.issues.map(show));

Konsolda:

text
(2) ['dishes.2: «osh» yuqorida bor', 'dishes.3: «manti» yuqorida bor']

path: ["dishes", 2] — massivning uchinchi elementi. Formada bu errors.dishes[2].message bo'ladi — xato aynan takror qator ostida (dinamik maydonlar darsida). ctx.issues.push({…}) — Zod 4 uslubi; eski kodda ctx.addIssue({…}) ni ko'rasiz, u ham ishlaydi.

refine va superRefine async funksiya ham qabul qiladi ("bu nom serverda bormi?"). Unda safeParse o'rniga safeParseAsync kerak. Bu — RHF III darsida, vazifalar dagi ko'p qadamli formada.

flowchart LR
  A["Kirish<br/>(satrlar)"] --> P["preprocess<br/>'' → undefined"]
  P --> T["Tur va qoidalar<br/>coerce, min, regex"]
  T --> TR["transform<br/>trim, raqamlar"]
  TR --> R["refine /<br/>superRefine"]
  R --> O["Chiqish<br/>(z.output)"]

Zanjir tartibi bo'yicha: preprocess — eng oldin, refine — oxirida. Obyekt refine i esa barcha maydonlar tekshirilib bo'lgach ishlaydi.

Tekshirib ko'ring: "Parol" va "Parolni takrorlang" maydonlari. Xato qaysi maydon ostida chiqishi kerak va buni qanday yozasiz?

Javob

"Parolni takrorlang" ostida: mehmon aynan shu maydonni noto'g'ri to'ldirgan deb hisoblanadi. Obyekt sxemasiga .refine((v) => v.password === v.repeat, { error: "Parollar mos emas", path: ["repeat"] }). path siz xato obyektga tegishli bo'lib qoladi va forma uni maydon ostida ko'rsatmaydi.

6. O'zbekcha xabarlar

6.1 Uch daraja

Zod'ning xabarlarini uch joyda o'zgartirish mumkin:

Daraja Qanday Qachon
har qoida .min(2, "Ism kamida 2 harf") yoki { error: "…" } asosiy yo'l — eng aniq matn
tur xatosi z.number({ error: (iss) => … }) "bo'sh" va "noto'g'ri tur" ni ajratish
global z.config(z.locales.uz()) xabar yozilmagan hamma joy

Funksiya ko'rinishidagi error qiymatni ko'rib xabar tanlaydi:

jsx
import { z } from "zod";

const Guests = z.number({
  error: (iss) =>
    iss.input === undefined ? "Maydonni to'ldiring" : "Son yozing",
});
console.log(Guests.safeParse(undefined).error.issues[0].message);
console.log(Guests.safeParse("to'rt").error.issues[0].message);

Konsolda:

text
Maydonni to'ldiring
Son yozing

6.2 Tayyor tarjima: z.locales.uz()

Zod 4 da o'zbekcha tarjima bor (Zod va turni sxemadan chiqarish darsida ko'rgansiz). U natija oynasida yo'q (u yerda faqat en), shuning uchun bu blok Node'da tekshirilgan:

ts
import { z } from "zod";

z.config(z.locales.uz());
const Name = z.string().min(2);
console.log(Name.safeParse("A").error?.issues[0]?.message);

Konsolda:

text
Juda kichik: kutilgan string >=2 belgi bo‘lishi kerak

Ikki sabab bilan biz global tarjimaga tayanmaymiz. Birinchisi — matn texnik ("kutilgan string >=2 belgi"), mehmon uchun emas. Ikkinchisi — u ‘ belgisini ishlatadi (bo‘lishi), kursda va vazifalar da esa faqat '. Global tarjima — "zaxira": siz yozishni unutgan joyda inglizcha chiqmasin. Asosiy matnlar — har qoidaning o'zida.

7. zod/mini — vazifalar dagi ko'rinish

7.1 Bir xil qoidalar, boshqa yozuv

vazifalar klassik zod emas, zod/mini ni ishlatadi (API qatlami darsidagi qaror: klassik +24.2 KB, mini +5.8 KB gzip). Mini'da metodlar zanjiri yo'q — qoidalar .check(...) ichida funksiya sifatida:

Klassik zod zod/mini
z.string().trim().min(1, "…") z.string().check(z.trim(), z.minLength(1, "…"))
.max(100, "…") z.maxLength(100, "…")
.refine(fn, "…") z.refine(fn, "…") (.check ichida)
S.optional() z.optional(S)
S.transform(fn) z.pipe(S, z.transform(fn))
obj.superRefine(fn) obj.check(z.superRefine(fn))

Bitta farq bor. Mini'da tarjimalar sukut bo'yicha yuklanmaydi — xabar yozilmagan qoida faqat "Invalid input" deydi:

jsx
import { z } from "zod/mini";

const Name = z.string().check(z.minLength(2));
console.log(Name.safeParse("A").error.issues[0].message);

const Named = z.string().check(z.minLength(2, "Ism kamida 2 harf"));
console.log(Named.safeParse("A").error.issues[0].message);

Konsolda:

text
Invalid input
Ism kamida 2 harf

Bu kichik hajmning narxi: tarjima matnlari to'plamga kirmaydi. Xulosa — mini'da har qoidaga xabar yozing. vazifalar aynan shunday qiladi.

8. Vazifalar qadami: tahrir formasining sxemasi (holat 32)

8.1 sxemalar/vazifa-formasi.ts

Oldingi darsda tahrir formasining skeletini yozdik — register("matn") qoidasiz. Qoidalar shu faylda (kanon, to'liq):

ts
// vazifa-formasi.ts — formalar sxemasi (19/#30–#31): React Hook
// Form shu sxemani resolver orqali ishlatadi. Qoidalar — ro'yxat
// formasidagi (holat.ts xatoniTop) bilan bir xil matnlar: bo'sh,
// juda uzun, takror.
// zod/mini (19/#12 qarori — hajm): zanjir o'rniga .check(...)
import { z } from "zod/mini";
import { Vazifa } from "../vazifa.ts";

/** Matn: chetlari kesiladi (transform), keyin tekshiriladi */
export function matnSxemasi(takrormi: (matn: string) => boolean) {
  return z.string().check(
    // z.trim() — qiymatni O'ZGARTIRADI (overwrite): forma
    // natijasida kesilgan matn; keyingi tekshiruvlar
    // kesilganiga qaraydi
    z.trim(),
    z.minLength(1, "Vazifa matnini yozing."),
    z.maxLength(
      Vazifa.MAKS_UZUNLIK,
      `Matn juda uzun (ko'pi bilan ${Vazifa.MAKS_UZUNLIK} belgi).`,
    ),
    z.refine(
      (matn) => !takrormi(matn),
      "Bu vazifa ro'yxatda allaqachon bor.",
    ),
  );
}

/**
 * Tahrirlash formasi (19/#31–#32). takrormi — shu vazifadan
 * BOSHQA vazifada shu matn bormi (sxema ro'yxatni bilmaydi —
 * tashqaridan)
 */
export function tahrirSxemasi(takrormi: (matn: string) => boolean) {
  return z.object({
    matn: matnSxemasi(takrormi),
    bajarildi: z.boolean(),
  });
}

export type TahrirQiymati = z.output<ReturnType<typeof tahrirSxemasi>>;

Bugungi darsning to'rt g'oyasi shu faylda:

  • z.trim() — overwrite. Mehmon " Sut olish " yozsa, forma natijasida "Sut olish". Keyingi uch qoida kesilgan matnni ko'radi: " " → "" → "Vazifa matnini yozing.".
  • Xabarlar — ro'yxat sahifasidagi tezkor formaniki bilan bir xil (holat.ts dagi xatoniTop: "Vazifa matnini yozing.", "Bu vazifa ro'yxatda allaqachon bor."). Ikki formada bitta xato ikki xil yozilsa, foydalanuvchi chalkashadi.
  • z.refine + tashqi funksiya. "Takrormi?" uchun ro'yxat kerak, sxema esa ro'yxatni bilmaydi. Shuning uchun sxema — funksiya: matnSxemasi(takrormi). Uni chaqirgan joy (forma) ro'yxatni biladi va tekshiruvchini beradi. Shu tufayli sxemani ro'yxatsiz, sof funksiya bilan test qilish mumkin.
  • z.output — TahrirQiymati, ya'ni saqlash funksiyasi oladigan tur. Kirish va chiqish bu yerda bir xil ko'rinadi (ikkalasi { matn: string; bajarildi: boolean }), lekin chiqishda matn kesilgan — qiymat farq qiladi.

Vazifa.MAKS_UZUNLIK — 100 (static a'zolar darsidagi statik maydon). Qoida bitta joyda: server javobini tekshiradigan api-sxema.ts ham shu sonni ishlatadi.

8.2 Test: tekshiruv/vazifa-formasi.test.js

Sxema React'siz test qilinadi — oddiy node:test (kanon):

js
// tekshiruv/vazifa-formasi.test.js — forma sxemasi (19/#30): trim
// (transform), bo'sh, uzun, takror (refine — tashqi funksiya bilan)
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import {
  matnSxemasi,
  tahrirSxemasi,
} from "../src/sxemalar/vazifa-formasi.ts";

const xabar = (natija) =>
  natija.success ? null : natija.error.issues.map((i) => i.message);

describe("vazifa formasi sxemasi", () => {
  const sxema = matnSxemasi((m) => m.toLowerCase() === "non olish");

  test("chetlari kesiladi — natija kesilgan matn", () => {
    assert.deepEqual(sxema.safeParse("  Sut olish  "), {
      success: true,
      data: "Sut olish",
    });
  });

  test("bo'sh va faqat bo'shliq — bitta xabar", () => {
    assert.deepEqual(xabar(sxema.safeParse("   ")), [
      "Vazifa matnini yozing.",
    ]);
  });

  test("101 belgi — uzun; 100 — to'g'ri", () => {
    assert.deepEqual(xabar(sxema.safeParse("x".repeat(101))), [
      "Matn juda uzun (ko'pi bilan 100 belgi).",
    ]);
    assert.equal(sxema.safeParse("x".repeat(100)).success, true);
  });

  test("takror (kesilgandan keyin tekshiriladi)", () => {
    assert.deepEqual(xabar(sxema.safeParse(" NON OLISH ")), [
      "Bu vazifa ro'yxatda allaqachon bor.",
    ]);
  });

  test("tahrir: bajarildi — faqat boolean", () => {
    const t = tahrirSxemasi(() => false);
    assert.equal(
      t.safeParse({ matn: "A", bajarildi: "ha" }).success,
      false,
    );
    assert.deepEqual(t.parse({ matn: " A ", bajarildi: true }), {
      matn: "A",
      bajarildi: true,
    });
  });
});

E'tibor bering: "bo'sh va faqat bo'shliq — bitta xabar". " " uchun minLength(1) buzildi, refine ham ishlashi mumkin edi. Lekin takrormi("") — false, shuning uchun xabar bitta. Test buni qotirib qo'yadi. To'rtinchi test — tartibning isboti: " NON OLISH " avval kesiladi, keyin takror deb topiladi (takrormi kichik harf bilan solishtiradi, xuddi matnBormi kabi).

.ts faylni node:test to'g'ridan import qiladi — Node 24 turlarni o'chirib ishlatadi.

8.3 Tekshiruv

Holat 32 da (kanon natijalar/tekshiruv-32.txt):

text
ℹ tests 182
ℹ pass 182
ℹ fail 0

Bu xom natija «Orqaga» tuzatishidan oldin olingan: 182 = oldingi 177 + shu fayldagi 5. Tuzatish (React Router III) har holatga yana 8 ta test qo'shgan, shuning uchun sizda shu qadamda 190 bo'ladi (oldingi 185 + 5). Commit hali yo'q: sxema formaga kelgusi darsda zodResolver bilan ulanadi.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
bo'sh maydon "0 kishi" bo'lib o'tdi z.coerce.number() — "" → 0 z.preprocess bilan "" → undefined
expected number, received string formadan satr keldi z.coerce.number() yoki RHF valueAsNumber
ixtiyoriy email bo'sh bo'lsa ham xato .optional() "" ni qabul qilmaydi z.union([z.literal(""), z.email()])
"Parollar mos emas" maydon ostida chiqmaydi refine da path yo'q { error, path: ["repeat"] }
ikki maydon xatosi kech chiqadi obyektda tur xatosi — refine ishlamadi when yoki maydonlarni avval to'g'rilash
"Invalid input" — boshqa hech narsa zod/mini, xabar yozilmagan har qoidaga xabar
"false" → true z.coerce.boolean() — Boolean("false") z.stringbool() yoki checkbox'dan boolean
string.email() eskirgan ogohlantirishi Zod 3 yozuvi z.email()

10. Mashqlar

Mashqlar kurs/mashqlar/19/30-zod/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): natijani ayting

Har qator nima beradi? Natija (son) yoki "xato" deb yozing.

  • z.coerce.number().parse("7") →
  • z.coerce.number().parse("") →
  • z.coerce.number().safeParse("yetti").success →
  • z.string().optional().safeParse("").success →
  • z.email().optional().safeParse("").success →
Yechim
  • 7 — satr songa aylandi.
  • 0 — Number("") — 0. Formadagi eng ko'p uchraydigan tuzoq.
  • false — Number("yetti") — NaN, Zod NaN ni son deb qabul qilmaydi.
  • true — "" ham satr; optional faqat undefined ga ruxsat qo'shadi.
  • false — "" email emas. optional uni "yo'q" deb hisoblamaydi.

Yechim fayli shu beshtasini konsolga chiqaradi.

2-mashq (o'rta): bron sxemasi

Bron formasi uchun Booking sxemasini yozing:

  1. name — kesiladi, kamida 2 harf: "Ism kamida 2 harf".
  2. phone — +998 90 000 00 00 ko'rinishi ("Namuna: +998 90 000 00 00"), natija — faqat raqamlar.
  3. guests — bo'sh bo'lsa "Nechta mehmon?", butun son 1–12.
  4. email — ixtiyoriy: bo'sh satr bo'lsa natijada undefined, aks holda to'g'ri email ("Email noto'g'ri").

Boshlang'ich kodda sinov ma'lumoti bor:

jsx
import { z } from "zod";

const Booking = z.object({
  // shu yerga
});

const form = {
  name: "  Malika ",
  phone: "+998 90 000 00 00",
  guests: "4",
  email: "",
};
console.log(Booking.safeParse(form));

Konsolda:

text
{success: true, data: {}}

Bo'sh z.object({}) noma'lum kalitlarni tashlab yuboradi — shuning uchun data bo'sh.

Yechim
jsx
import { z } from "zod";

const emptyToUndefined = (v) => (v === "" ? undefined : v);
const PHONE = /^\+998 \d{2} \d{3} \d{2} \d{2}$/;

const Booking = z.object({
  name: z.string().trim().min(2, "Ism kamida 2 harf"),
  phone: z
    .string()
    .trim()
    .regex(PHONE, "Namuna: +998 90 000 00 00")
    .transform((s) => s.replace(/\D/g, "")),
  guests: z.preprocess(
    emptyToUndefined,
    z.coerce
      .number({ error: "Nechta mehmon?" })
      .int("Butun son yozing")
      .min(1, "Kamida 1 kishi")
      .max(12, "Ko'pi bilan 12 kishi"),
  ),
  email: z.preprocess(
    emptyToUndefined,
    z.email("Email noto'g'ri").optional(),
  ),
});

const form = {
  name: "  Malika ",
  phone: "+998 90 000 00 00",
  guests: "4",
  email: "",
};
console.log(Booking.safeParse(form));
const bad = Booking.safeParse({ ...form, guests: "", email: "ali@" });
console.log(z.flattenError(bad.error).fieldErrors);

Konsolda:

text
{success: true, data: {name: 'Malika', phone: '998900000000', guests: 4, email: undefined}}
{guests: ['Nechta mehmon?'], email: ["Email noto'g'ri"]}

emptyToUndefined ikki marta ishlatildi — bitta yordamchi funksiya, ikki maydon. email: undefined natijada bor: preprocess "" ni undefined qildi, optional uni qabul qildi.

3-mashq (qiyin): buyurtma — bir nechta xato

Buyurtma: items — { dish, qty } lar massivi. Qoidalar:

  1. dish — "osh", "manti" yoki "lagmon" (z.enum); qty — formadan satr, butun son 1–10.
  2. Bir taom ikki marta bo'lmasin: takror qatorning dish maydoniga "Bu taom yuqorida bor".
  3. Jami porsiya 20 dan oshmasin: items ning o'ziga "Bir buyurtmada ko'pi bilan 20 porsiya".

Ishora: 2 va 3 — superRefine da, ctx.issues.push. Massiv elementi maydoniga yo'l — ["items", i, "dish"]. qty uchun z.coerce.number() yetarli: bo'sh qator bu mashqda yo'q.

Yechim
jsx
import { z } from "zod";

const Item = z.object({
  dish: z.enum(["osh", "manti", "lagmon"], {
    error: "Taomni tanlang",
  }),
  qty: z.coerce.number().int().min(1).max(10, "Ko'pi bilan 10"),
});

const Order = z
  .object({ items: z.array(Item).min(1, "Kamida bitta taom") })
  .superRefine((order, ctx) => {
    const dishes = order.items.map((item) => item.dish);
    dishes.forEach((dish, i) => {
      if (dishes.indexOf(dish) !== i) {
        ctx.issues.push({
          code: "custom",
          input: dish,
          path: ["items", i, "dish"],
          message: "Bu taom yuqorida bor",
        });
      }
    });
    const total = order.items.reduce((sum, it) => sum + it.qty, 0);
    if (total > 20) {
      ctx.issues.push({
        code: "custom",
        input: total,
        path: ["items"],
        message: "Bir buyurtmada ko'pi bilan 20 porsiya",
      });
    }
  });

const r = Order.safeParse({
  items: [
    { dish: "osh", qty: "8" },
    { dish: "manti", qty: "9" },
    { dish: "osh", qty: "5" },
  ],
});
const show = (i) => `${i.path.join(".")}: ${i.message}`;
console.log(r.error.issues.map(show));
console.log(Order.parse({ items: [{ dish: "manti", qty: "3" }] }));

Konsolda:

text
(2) ['items.2.dish: Bu taom yuqorida bor', "items: Bir buyurtmada ko'pi bilan 20 porsiya"]
{items: [{dish: 'manti', qty: 3}]}

Ikkita xato bir vaqtda — refine bu yerda yetmasdi. Ikkinchi qator — to'g'ri buyurtma: qty endi son (3), satr emas. reduce — massivni yig'ish.

4-mashq: Vazifalar qadami

src/sxemalar/vazifa-formasi.ts va tekshiruv/vazifa-formasi.test.js ni kanondagidek yozing. npm test — node:test 190 (5 tasi yangi; «Orqaga» tuzatishigacha — 182), hammasi pass.

Yechim

Tekshiruv ro'yxati:

  • npm test — ℹ tests 190 · ℹ pass 190 (oldin 185; «Orqaga» tuzatishigacha 177 → 182).
  • Testni ataylab buzing: z.trim() ni z.minLength dan keyinga o'tkazing. "bo'sh va faqat bo'shliq" testi yiqiladi — " " uzunligi 3, minLength(1) o'tadi. Tartib qaytarilgach yana yashil.
  • npm run tip — TahrirQiymati { matn: string; bajarildi: boolean }.
  • Commit hali qilinmaydi — forma RHF + Zod darsida ulanadi.

11. Real ishda

  • Bitta sxema — ikki tomon. Next.js server action'ida (Server Actions II) yoki Express'da (Zod serverda) aynan shu sxema qayta tekshiradi. Brauzerdagi tekshiruv mehmon uchun tez javob, serverdagi — himoya.
  • coerce — query va env uchun ham: URL parametri (?sahifa=2) va muhit o'zgaruvchilari ham satr. z.coerce.number() ular uchun ham kerak, lekin "bo'sh" holatini doim o'ylang.
  • Muqobillar: Valibot va ArkType xuddi shu g'oyalarni beradi — Muqobillar darsida o'lchov bilan.
  • Intervyu savollari: "z.input va z.output farqi?", "refine va superRefine farqi?", "coerce ning xavfi nima?", "nega obyekt refine i ba'zan ishlamaydi?".

Xulosa

  • Formadan hamma narsa satr keladi, bo'sh maydon — "". z.coerce.number() uni 0 qiladi — z.preprocess bilan "" → undefined.
  • optional faqat undefined ni qabul qiladi; bo'sh ixtiyoriy maydon — z.union([z.literal(""), …]) yoki preprocess.
  • transform — tekshiruvdan keyin o'zgartiradi; sxemaning kirish (z.input) va chiqish (z.output) turlari farq qiladi.
  • refine + path — ikki maydonni solishtirish; tur xatosi bo'lsa ishlamaydi (when). superRefine — bir nechta xato, ctx.issues.push.
  • Xabarlar — har qoidaning o'zida, o'zbekcha. zod/mini da xabarsiz qoida faqat "Invalid input" deydi.

Keyingi dars: RHF + Zod: resolver va umumiy sxemalar — sxemani zodResolver bilan formaga ulash, kirish/chiqish turlari formada, server xatosini setError bilan ko'rsatish.

Manbalar

  • Zod 4: Basic usage, Defining schemas (coerce, optional, transform, refine, superRefine) — zod.dev/api (2026-10 holatiga, v4.6.5)
  • Zod 4: Customizing errors (error parametri, z.config, locales) — zod.dev/error-customization
  • Zod 4: Formatting errors (z.flattenError, z.prettifyError) — zod.dev/error-formatting
  • Zod Mini — zod.dev/packages/mini
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zod formalar uchun: coerce, refine, superRefine va transform — IlmHamroh