الكتابات/tutorial/2026/06
Tutorial8 يونيو 2026·24 دقيقة

Valibot: التحقق المعياري الآمن للأنواع مع Next.js و TypeScript (2026)

تعلّم كيفية التحقق من البيانات من الطرف إلى الطرف باستخدام Valibot، مكتبة المخططات المعيارية التي تبدأ بأقل من 1 كيلوبايت. نغطّي المخططات والأنابيب والتحويلات والفحوصات غير المتزامنة وتسطيح الأخطاء ونماذج Server Action في Next.js — مع استنتاج كامل للأنواع في TypeScript.

التحقق من البيانات هو الحدّ الفاصل بين تطبيقك وفوضى العالم الخارجي — مدخلات النماذج، وحمولات واجهات API، ومتغيرات البيئة، و webhooks الطرف الثالث. إذا كانت مكتبة التحقق ثقيلة، فستدفع ثمنها مع كل تحميل للصفحة. وإذا لم تكن آمنة للأنواع، فستدفع ثمنها على شكل أخطاء في الإنتاج.

تحلّ Valibot كلتا المشكلتين. إنها مكتبة مخططات آمنة للأنواع بالكامل تبدأ بحجم يقارب 1.3 كيلوبايت مضغوطة، وتنمو فقط بحسب أدوات التحقق التي تستوردها فعليًا. ولأن كل أداة تحقق هي دالة منفصلة، فإن أدوات التجميع تزيل عبر تقنية tree-shaking كل ما لا تستخدمه. والنتيجة طبقة تحقق تبدو وكأنها Zod لكنها تشحن جزءًا ضئيلًا من JavaScript.

في هذا الدرس ستبني طبقة تحقق كاملة لتطبيق Next.js 15: مخططات، وأنابيب تحويل، وقواعد مخصّصة وغير متزامنة، ومعالجة أخطاء، ونموذج Server Action متحقَّق منه بالكامل.

المتطلبات المسبقة

قبل البدء، تأكّد من توفّر:

  • Node.js 20+ مثبّت
  • مشروع Next.js 15 يستخدم App Router (أو أي مشروع TypeScript)
  • معرفة أساسية بـ TypeScript من حيث الأنواع العامة والاستنتاج
  • محرّر شيفرة (يُنصح بـ VS Code)

ما الذي ستبنيه

ميزة "تسجيل مطوّر" صغيرة تتضمّن:

  • وحدة مخطط قابلة لإعادة الاستخدام تتحقق من الأسماء والبريد الإلكتروني وكلمات المرور والأدوار والوسوم
  • استنتاج للأنواع بحيث لا تُعيد معالجاتك تعريف الواجهات أبدًا
  • إجراء Server Action يحلّل FormData ويُعيد أخطاء على مستوى الحقل
  • تحقّق غير متزامن يفحص البريد الإلكتروني مقابل قاعدة بيانات
  • تحقّق من متغيرات البيئة يفشل مبكرًا عند بدء التشغيل

في النهاية ستفهم النموذج الذهني لـ Valibot بما يكفي للتحقق من أي شيء.

الخطوة 1: إعداد المشروع

ثبّت Valibot. ليس لها أي اعتماديات، لذا فهي حزمة صغيرة واحدة:

npm install valibot
# أو
pnpm add valibot

تشحن Valibot بنسختي ESM و CJS وأنواع TypeScript كاملة. لا حاجة لأي إعداد.

أهم شيء ينبغي معرفته عن Valibot هو أسلوب الاستيراد الخاص بها. فبدلًا من كائن كبير يحتوي على توابع، كل دالة هي تصدير مُسمّى. والعرف هو استيراد المساحة الاسمية كاملة باسم v:

import * as v from 'valibot';

هذا ما يجعل tree-shaking يعمل: عندما يلاحظ أداة التجميع أنك استخدمت فقط v.string و v.object، فإنها تُسقط أكثر من 200 أداة تحقق أخرى من حزمتك.

الخطوة 2: مخططك الأول

يصف المخطط بنية البيانات الصالحة. لنبدأ بمخطط تسجيل دخول:

import * as v from 'valibot';
 
const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

هناك فكرتان تقومان بالعمل هنا:

  1. v.object يعرّف مخطط كائن بإدخالات مُحدّدة الأنواع.
  2. v.pipe يربط مخططًا أساسيًا بإجراء واحد أو أكثر (تحقّقات وتحويلات). اقرأ v.pipe(v.string(), v.email()) على أنه: "يجب أولًا أن يكون نصًا، ثم يجب أن يبدو كبريد إلكتروني."

هذا التركيب هو جوهر Valibot. يُنشئ مخطط أساسي (v.string، v.number، v.boolean، v.array، v.object) النوع. والإجراءات الموصولة بعده تنقّح القيمة دون تغيير النوع.

الخطوة 3: تحليل البيانات

يبقى المخطط خاملًا حتى تمرّر البيانات عبره. تمنحك Valibot طريقتين للقيام بذلك.

parse — يرمي خطأً عند الفشل

// يُعيد القيمة المُحدّدة النوع، أو يرمي ValiError
const output = v.parse(LoginSchema, {
  email: 'jane@example.com',
  password: '12345678',
});

استخدم parse عندما تكون البيانات غير الصالحة استثنائية فعلًا — مثل متغير بيئة يجب أن يكون موجودًا حتى يقلع التطبيق.

safeParse — يُعيد نتيجة

const result = v.safeParse(LoginSchema, {
  email: 'jane@example.com',
  password: '12345678',
});
 
if (result.success) {
  // result.output مُحدّد النوع بالكامل
  console.log(result.output.email);
} else {
  // result.issues مصفوفة من مشكلات التحقق
  console.log(result.issues);
}

لا يرمي safeParse أي خطأ أبدًا. إنه يُعيد اتحادًا مُميّزًا يحتوي على قيمة success منطقية، و output عند النجاح، و issues عند الفشل. هذه هي الأداة المناسبة للمدخلات الموجّهة للمستخدم حيث تكون الأخطاء متوقّعة ويجب عرضها.

فضّل safeParse لأي شيء يلمسه المستخدم (النماذج، معاملات الاستعلام، أجسام الطلبات) و parse للقيم الموثوقة لكن المطلوبة (الإعدادات، متغيرات البيئة). إن رمي خطأ بسبب خطأ مطبعي في نموذج اتصال تجربة سيئة؛ بينما رمي خطأ بسبب رابط قاعدة بيانات مفقود عند بدء التشغيل أمر صحيح تمامًا.

الخطوة 4: استنتاج أنواع TypeScript

هنا تؤتي Valibot ثمارها مرّتين. لست بحاجة أبدًا لكتابة واجهة منفصلة — فالمخطط هو مصدر الحقيقة:

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});
 
// { email: string; password: string }
type LoginData = v.InferOutput<typeof LoginSchema>;
 
function authenticate(data: LoginData) {
  // data.email و data.password مُحدّدا النوع ومُتحقَّق منهما مسبقًا
}

هناك مساعدان للاستنتاج، والفرق بينهما يصبح مهمًّا بمجرد إضافة تحويلات:

  • v.InferOutput — النوع بعد التحليل والتحويل. هذا ما تستهلكه شيفرتك.
  • v.InferInput — النوع قبل التحليل. هذا ما يجب أن يوفّره المُستدعي.

تأمّل مخططًا يحوّل نصًا إلى طوله:

const ObjectSchema = v.object({
  key: v.pipe(
    v.string(),
    v.transform((input) => input.length)
  ),
});
 
type Input = v.InferInput<typeof ObjectSchema>;  // { key: string }
type Output = v.InferOutput<typeof ObjectSchema>; // { key: number }

المدخل نصّ؛ والمخرج رقم. استخدم InferInput لتحديد أنواع وسائط دوالك و InferOutput لتحديد نوع النتيجة المُحلَّلة.

الخطوة 5: بناء مخطط واقعي

لنصمّم مخطط تسجيل المطوّر. أنشئ lib/schemas.ts:

import * as v from 'valibot';
 
export const RoleSchema = v.picklist(['frontend', 'backend', 'fullstack']);
 
export const RegistrationSchema = v.object({
  name: v.pipe(
    v.string(),
    v.trim(),
    v.nonEmpty('الرجاء إدخال اسمك.'),
    v.maxLength(60, 'الاسم طويل جدًا.')
  ),
  email: v.pipe(
    v.string(),
    v.trim(),
    v.toLowerCase(),
    v.email('الرجاء إدخال بريد إلكتروني صالح.')
  ),
  password: v.pipe(
    v.string(),
    v.minLength(8, 'يجب ألا تقل كلمة المرور عن 8 أحرف.'),
    v.regex(/[A-Z]/, 'أدرج حرفًا كبيرًا واحدًا على الأقل.'),
    v.regex(/[0-9]/, 'أدرج رقمًا واحدًا على الأقل.')
  ),
  role: RoleSchema,
  tags: v.pipe(
    v.array(v.pipe(v.string(), v.nonEmpty())),
    v.maxLength(5, 'لا يُسمح بأكثر من 5 وسوم.')
  ),
  newsletter: v.optional(v.boolean(), false),
});
 
export type Registration = v.InferOutput<typeof RegistrationSchema>;

بضع عناصر جديدة جديرة بالإبراز:

  • v.picklist يقيّد القيمة إلى مجموعة ثابتة من القيم الحرفية — مثالي للتعدادات مثل الأدوار.
  • v.trim و v.toLowerCase هما إجراءا تحويل. إنهما يُطبّعان القيمة أثناء تدفّقها عبر الأنبوب، فيصبح " JANE@EXAMPLE.COM " هو "jane@example.com" قبل أن يعمل فحص البريد الإلكتروني.
  • v.optional(schema, fallback) يجعل الحقل اختياريًا ويوفّر قيمة افتراضية، فيكون newsletter دائمًا قيمة منطقية في المخرَج.
  • يأخذ كل إجراء تحقّق رسالة اختيارية كوسيطه الأخير. اضبط هذه الرسائل بعناية — فهي تصبح نصّ الخطأ الموجّه لمستخدمك.

الخطوة 6: التحقق المخصّص باستخدام check

تغطّي الإجراءات المدمجة معظم الحالات، لكن القواعد عبر الحقول تحتاج منطقًا مخصّصًا. يُشغّل إجراء v.check مُسنِدًا اعتباطيًا ويفشل برسالتك إن أعاد false:

import * as v from 'valibot';
 
const SignUpSchema = v.pipe(
  v.object({
    password: v.pipe(v.string(), v.minLength(8)),
    confirmPassword: v.string(),
  }),
  v.check(
    (input) => input.password === input.confirmPassword,
    'كلمتا المرور غير متطابقتين.'
  )
);

لاحظ أن v.check يقع خارج v.object، مُغلَّفًا بـ v.pipe خارجي. وذلك لأنه يحتاج الكائن كاملًا لمقارنة حقلين. الفحوصات على مستوى الحقل تذهب داخل أنبوب الحقل؛ والفحوصات على مستوى الكائن تُغلّف الكائن.

إليك مثالًا آخر يتحقق من أن الطول المُعلَن لمصفوفة يطابق محتواها:

const CustomObjectSchema = v.pipe(
  v.object({
    list: v.array(v.string()),
    length: v.number(),
  }),
  v.check(
    (input) => input.list.length === input.length,
    'القائمة لا تطابق الطول.'
  )
);

الخطوة 7: التحقق غير المتزامن

بعض القواعد لا يمكن الإجابة عنها إلا عبر قاعدة بيانات أو واجهة API خارجية — مثلًا، "هل هذا البريد مُستخدَم بالفعل؟" تدعم Valibot المخططات غير المتزامنة عبر متغيرات غير متزامنة من دوالها: v.checkAsync و v.pipeAsync و v.objectAsync وتابعَي v.parseAsync / v.safeParseAsync.

import * as v from 'valibot';
 
async function isEmailAvailable(email: string): Promise<boolean> {
  // استبدلها باستعلام حقيقي من قاعدة البيانات
  const taken = await db.user.findUnique({ where: { email } });
  return taken === null;
}
 
const UniqueEmailSchema = v.pipeAsync(
  v.string(),
  v.email('الرجاء إدخال بريد إلكتروني صالح.'),
  v.checkAsync(isEmailAvailable, 'هذا البريد الإلكتروني مُسجَّل بالفعل.')
);
 
// يجب انتظار مخطط غير متزامن:
const result = await v.safeParseAsync(UniqueEmailSchema, 'jane@example.com');

شغّل التحقّقات المتزامنة قبل غير المتزامنة في الأنبوب ذاته. تتوقّف Valibot افتراضيًا عند أول إجراء فاشل، فترفض الفحوصات الرخيصة (التنسيق، الطول) المدخلات السيئة بوضوح قبل أن تنفق رحلة ذهاب وإياب إلى قاعدة البيانات عليها.

القاعدة العملية: إن كان أي إجراء في الأنبوب غير متزامن، يصبح المخطط بأكمله غير متزامن، ويجب أن تستخدم توابع التحليل *Async. أبقِ المخططات غير المتزامنة منفصلة عن المخططات المتزامنة بحتة حتى لا تدفع كلفة عدم التزامن إلا حيث تحتاجها.

الخطوة 8: معالجة الأخطاء باستخدام flatten

عندما يفشل safeParse، يكون result.issues مصفوفة مسطّحة بكل مشكلة وُجدت، لكل منها message و path. في النموذج، تريد الأخطاء مُجمَّعة حسب الحقل بدلًا من ذلك. يقوم المساعد v.flatten بذلك بالضبط:

import * as v from 'valibot';
 
const FormSchema = v.object({
  name: v.pipe(v.string(), v.nonEmpty('الاسم مطلوب.')),
  address: v.object({
    city: v.pipe(v.string(), v.nonEmpty('المدينة مطلوبة.')),
    zip: v.pipe(v.string(), v.regex(/^\d{5}$/, 'رمز بريدي غير صالح.')),
  }),
});
 
const result = v.safeParse(FormSchema, {
  name: '',
  address: { city: '', zip: 'abc' },
});
 
if (!result.success) {
  const flat = v.flatten<typeof FormSchema>(result.issues);
  console.log(flat);
  // {
  //   nested: {
  //     name: ['الاسم مطلوب.'],
  //     'address.city': ['المدينة مطلوبة.'],
  //     'address.zip': ['رمز بريدي غير صالح.'],
  //   }
  // }
}

يُعيد flatten ما يصل إلى ثلاثة مفاتيح: root (مشكلات على القيمة العليا)، و nested (مشكلات مُفهرسة بمسار مفصول بنقاط)، و other (مشكلات بلا مسار). لمعظم النماذج تقرأ nested فقط، رابطًا كل مسار بالمدخل المطابق.

الخطوة 9: إجراء Server Action متحقَّق منه في Next.js

الآن نربط كل شيء. في مشروع Next.js 15 يعتمد App Router، يستقبل Server Action قيمة FormData، ويتحقق منها باستخدام Valibot، ويُعيد حالة مُحدَّدة النوع إلى العميل.

أنشئ app/register/actions.ts:

'use server';
 
import * as v from 'valibot';
import { RegistrationSchema } from '@/lib/schemas';
 
export type FormState = {
  ok: boolean;
  errors?: Record<string, [string, ...string[]]>;
  message?: string;
};
 
export async function registerAction(
  _prev: FormState,
  formData: FormData
): Promise<FormState> {
  // قيم FormData نصوص؛ أعِد تشكيلها قبل التحليل.
  const raw = {
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
    role: formData.get('role'),
    tags: formData.getAll('tags'),
    newsletter: formData.get('newsletter') === 'on',
  };
 
  const result = v.safeParse(RegistrationSchema, raw);
 
  if (!result.success) {
    const flat = v.flatten<typeof RegistrationSchema>(result.issues);
    return { ok: false, errors: flat.nested ?? {} };
  }
 
  // result.output كائن Registration مُحدَّد النوع ومُطبَّع بالكامل
  await saveDeveloper(result.output);
 
  return { ok: true, message: 'مرحبًا بك معنا!' };
}

يستخدم مكوّن النموذج useActionState من React 19 لربط الإجراء وعرض أخطاء الحقول:

'use client';
 
import { useActionState } from 'react';
import { registerAction, type FormState } from './actions';
 
const initial: FormState = { ok: false };
 
export function RegisterForm() {
  const [state, action, pending] = useActionState(registerAction, initial);
 
  return (
    <form action={action} className="space-y-4">
      <div>
        <input name="name" placeholder="الاسم الكامل" />
        {state.errors?.name && <p className="error">{state.errors.name[0]}</p>}
      </div>
 
      <div>
        <input name="email" type="email" placeholder="البريد الإلكتروني" />
        {state.errors?.email && <p className="error">{state.errors.email[0]}</p>}
      </div>
 
      <div>
        <input name="password" type="password" placeholder="كلمة المرور" />
        {state.errors?.password && (
          <p className="error">{state.errors.password[0]}</p>
        )}
      </div>
 
      <select name="role">
        <option value="frontend">واجهة أمامية</option>
        <option value="backend">واجهة خلفية</option>
        <option value="fullstack">متكامل</option>
      </select>
 
      <label>
        <input type="checkbox" name="newsletter" /> الاشتراك في النشرة البريدية
      </label>
 
      <button disabled={pending}>
        {pending ? 'جارٍ الإرسال...' : 'تسجيل'}
      </button>
 
      {state.ok && <p className="success">{state.message}</p>}
    </form>
  );
}

يحرس المخطط نفسه RegistrationSchema الآن العميل والخادم معًا. ولأن المخطط هو مصدر الحقيقة الوحيد، فلا يوجد انحراف بين ما يقبله النموذج وما يثق به معالجك.

الخطوة 10: التحقق من متغيرات البيئة

من حالات فشل الإنتاج الكلاسيكية متغير بيئة مفقود أو مُشوَّه يُكتشَف وقت التشغيل. تحقق منها مرّة واحدة، عند تحميل الوحدة، باستخدام parse بحيث يرفض التطبيق الإقلاع على إعداد سيّئ. أنشئ lib/env.ts:

import * as v from 'valibot';
 
const EnvSchema = v.object({
  DATABASE_URL: v.pipe(v.string(), v.url('يجب أن يكون DATABASE_URL رابطًا صالحًا.')),
  PORT: v.pipe(
    v.optional(v.string(), '3000'),
    v.transform(Number),
    v.number(),
    v.minValue(1)
  ),
  NODE_ENV: v.picklist(['development', 'production', 'test']),
});
 
// يرمي خطأً وقت الاستيراد إن كان أي شيء مفقودًا أو مُشوَّهًا.
export const env = v.parse(EnvSchema, process.env);

يلتقط هذا النمط سوء الإعداد قبل خدمة أي طلب واحد. لاحظ كيف يحوّل v.transform(Number) نصّ PORT إلى رقم حقيقي، ثم يتحقق v.number() و v.minValue(1) التاليان من القيمة المُحوَّلة — عرض نظيف لمزج التحويلات والتحقّقات في أنبوب واحد.

الخطوة 11: تركيب المخططات وإعادة استخدامها

مع نموّ تطبيقك، تُركّب المخططات بدلًا من إعادة كتابتها. تشحن Valibot أدوات تعكس عوامل الأنواع في TypeScript نفسها:

import * as v from 'valibot';
 
const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.string(),
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});
 
// عرض عام بدون كلمة المرور
const PublicUserSchema = v.omit(UserSchema, ['password']);
 
// فقط الحقول التي يحتاجها نموذج التسجيل
const SignUpSchema = v.pick(UserSchema, ['name', 'email', 'password']);
 
// كل شيء اختياري، لنقطة نهاية PATCH
const UserPatchSchema = v.partial(v.omit(UserSchema, ['id']));

للقيم التي قد تتخذ أشكالًا متعددة، يختار v.variant فرعًا عبر مفتاح مُميِّز — مثالي للاتحادات الموسومة مثل أحداث webhook:

const EventSchema = v.variant('type', [
  v.object({ type: v.literal('created'), id: v.string() }),
  v.object({ type: v.literal('deleted'), id: v.string(), reason: v.string() }),
]);

تُبقي هذه الأدوات مخططًا قانونيًا واحدًا وتشتقّ منه كل تنويعة، فيتردّد أي تغيير على UserSchema في كل مكان تلقائيًا.

الخطوة 12: التوافق مع Standard Schema

تطبّق Valibot مواصفة Standard Schema — واجهة مشتركة تبنّتها Zod و ArkType وغيرها. عمليًا يعني هذا أن مكتبات النماذج والموجّهات تقبل مخطط Valibot مباشرة، دون أي مُحوِّل:

// يعمل مع TanStack Form و react-hook-form (عبر المُحلِّل القياسي)
// و tRPC وأي أداة تتحدث Standard Schema.
import { useForm } from '@tanstack/react-form';
import { RegistrationSchema } from '@/lib/schemas';
 
const form = useForm({
  validators: { onChange: RegistrationSchema },
});

هذا التوافق هو سبب إمكانية تبنّي Valibot تدريجيًا: بدّلها خلف الواجهة نفسها التي تتوقّعها أدواتك بالفعل.

اختبار تنفيذك

تحقّق من السلوكيات الأساسية باختبار سريع باستخدام مُشغّل اختبارك المفضّل:

import * as v from 'valibot';
import { describe, it, expect } from 'vitest';
import { RegistrationSchema } from '@/lib/schemas';
 
describe('RegistrationSchema', () => {
  it('يطبّع المدخلات الصالحة ويقبلها', () => {
    const result = v.safeParse(RegistrationSchema, {
      name: '  Jane  ',
      email: 'JANE@EXAMPLE.COM',
      password: 'Secret123',
      role: 'fullstack',
      tags: ['react'],
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.output.name).toBe('Jane');
      expect(result.output.email).toBe('jane@example.com');
      expect(result.output.newsletter).toBe(false);
    }
  });
 
  it('يُبلّغ عن أخطاء على مستوى الحقل', () => {
    const result = v.safeParse(RegistrationSchema, {
      name: '',
      email: 'nope',
      password: 'short',
      role: 'fullstack',
      tags: [],
    });
    expect(result.success).toBe(false);
    if (!result.success) {
      const flat = v.flatten<typeof RegistrationSchema>(result.issues);
      expect(flat.nested?.name).toBeDefined();
      expect(flat.nested?.email).toBeDefined();
    }
  });
});

شغّله وتأكّد من نجاح كلتا الحالتين. تؤكّد الأولى أن التحويلات تعمل (trim، lowercase، القيمة الافتراضية)؛ وتؤكّد الثانية أن flatten يُنتج رسائل مُفهرسة بالحقل.

استكشاف الأخطاء وإصلاحها

"Type instantiation is excessively deep" على مخطط كبير. يأتي هذا عادةً من سلاسل v.pipe المتداخلة بعمق. استخرج المخططات الفرعية إلى ثوابت مُسمّاة وارجِع إليها؛ فالأجزاء الأصغر تُفحَص أنواعها أسرع وتُقرأ أفضل.

فحص غير متزامن لا يعمل أبدًا. على الأرجح استدعيت v.safeParse بدلًا من v.safeParseAsync، أو بنيت الأنبوب بـ v.pipe بدلًا من v.pipeAsync. أي إجراء غير متزامن يتطلّب تابع التحليل غير المتزامن والأنبوب غير المتزامن.

InferInput و InferOutput يختلفان على نحو غير متوقّع. هذا مقصود عند استخدامك v.transform. حدّد نوع وسائط دالتك بـ InferInput (ما يرسله المُستدعون) ونتائجك بـ InferOutput (ما يستقبلونه بعد التحليل).

حزمة أكبر مما هو متوقّع. تأكّد من استيرادك بصيغة import * as v from 'valibot' ودَع أداة التجميع تُجري tree-shaking. تجنّب إعادة تصدير المساحة الاسمية كاملة من ملف برميلي، فقد يُبطل ذلك إزالة الشيفرة الميتة.

الخطوات التالية

  • أضف v.brand لإنشاء أنواع اسمية (نوع UserId لا يمكن استبداله بأي نص آخر).
  • استكشف v.fallback للتعافي بأناقة من القيم غير الصالحة بدلًا من الفشل.
  • اربط مخططاتك في واجهة tRPC أو Hono بحيث يُتحقَّق من أجسام الطلبات عند الحافة.
  • قارن أثر الحزمة مقابل أداة تحققك الحالية بأداة مثل bundlejs لقياس التوفير كمّيًا.

الخاتمة

تمنحك Valibot أناقة بمستوى Zod واستنتاجًا كاملًا للأنواع في TypeScript بينما تشحن جزءًا ضئيلًا من JavaScript، بفضل تصميمها المعياري القابل لـ tree-shaking. لديك الآن نمط كامل: عرّف المخططات مرّة واحدة، اشتقّ الأنواع بـ InferOutput، حلّل بأمان بـ safeParse، جمّع الأخطاء بـ flatten، عالج القواعد غير المتزامنة بعائلة *Async، واحرس Server Action في Next.js من الطرف إلى الطرف.

الدرس الأكبر معماري: عندما يكون مخطط واحد هو مصدر الحقيقة لأنواعك ونماذجك وواجهة API وإعداداتك، تصبح فئات كاملة من الأخطاء غير ممكنة ببساطة. تحقّق عند الحدّ، واستنتج في كل مكان آخر، ودَع المُصرِّف يحمل الباقي.