لماذا ArkType؟
كل مكتبة تحقّق في وقت التشغيل تطلب منك تعلّم لغة ثانية. في Zod تكتب z.object({ name: z.string() })، وفي Valibot تكتب v.object({ name: v.string() }). كلاهما يعمل — لكن لا أحد منهما يشبه لغة TypeScript التي تكتبها كل يوم.
تراهن ArkType على العكس تماماً. أنت تكتب النوع نفسه:
const User = type({ name: "string" })النص "string" ليس كلمة مفتاحية سحرية. إنه تعبير نوع TypeScript يُحلَّل ويُدقَّق بواسطة مُصرِّف TypeScript نفسه أثناء الكتابة. اكتب خطأً مطبعياً مثل "strng" فيضع محررك خطاً أحمر تحته قبل أن تشغّل الكود أصلاً. واكتب "string | number" فتحصل على اتحاد مُميَّز في وقت التشغيل واستنتاج ثابت من النوع string | number — من الأحرف الستة نفسها.
الهدف التصميمي هو التماثل: تعريف واحد، صحيح في العالمين. والمكسب أن مخططاتك تتوقف عن كونها كوناً موازياً عليك مزامنته يدوياً مع أنواعك.
يبني هذا الدرس تطبيق Next.js 15 صغيراً لكنه متكامل باستخدام ArkType 2.2 — الإصدار الذي أضاف الدوال المُتحقَّق منها، ومجموعات التقاط regex مُصنّفة الأنواع، والعوامل متعددة المعاملات، والتوافق مع Standard Schema.
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبّت
- TypeScript 5.1 أو أحدث (يُنصح بـ 5.9+، فـ ArkType تعتمد بشدة على الاستنتاج الحديث)
- إلمام بـ Next.js App Router و Server Actions
- محرر كود بخادم لغة TypeScript يعمل (VS Code أو WebStorm أو Zed)
- فهم أساسي للتحقق في وقت التشغيل — إن سبق أن استخدمت Zod أو Valibot فأنت جاهز تماماً
ما الذي ستبنيه
ميزة "تقديم مداخلة مؤتمر" مع تحقّق عند كل حدود:
- مخطط مُصنّف لطلبات المداخلات، مكتوب بصياغة TypeScript
- خط morph يحوّل نصوص
FormDataالخام إلى أرقام وتواريخ ومعرّفات slug حقيقية - استخراج بيانات مهيكلة من رمز الجلسة عبر regex مُصنّف الأنواع
- نطاق (scope) قابل لإعادة الاستخدام يُشارَك بين العميل والخادم
- Server Action مُتحقَّق منه يعيد أخطاءً على مستوى الحقل
- متغيرات بيئة مُتحقَّق منها تفشل عند الإقلاع لا في الإنتاج
- تصدير JSON Schema لتوثيق واجهتك البرمجية العامة
كل شيء يمرّ بفحص الأنواع من طرف إلى طرف. لا واجهات مكتوبة يدوياً، ولا أشكال مكررة.
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js جديداً وثبّت ArkType:
npx create-next-app@latest talk-portal --typescript --app --tailwind --eslint
cd talk-portal
npm install arktypeArkType اعتمادية واحدة بلا أي حزم تشغيل خاصة بها. أضف جسر JSON Schema الاختياري الآن — ستستخدمه في الخطوة 12:
npm install @ark/json-schemaافتح tsconfig.json وتأكد من تفعيل وضع strict، فاستنتاج ArkType يعتمد عليه:
{
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"moduleResolution": "bundler"
}
}إذا كان strict مُعطّلاً، فستظل ArkType تتحقق بشكل صحيح في وقت التشغيل لكن استنتاجها الثابت يتدهور بشدة — إذ تتوسّع المفاتيح الاختيارية ومخرجات الـ morph إلى any في مواضع عدة. فعّله قبل أن تكتب مخططاً واحداً.
الخطوة 2: أول نوع لك
أنشئ الملف lib/schemas/talk.ts:
import { type } from "arktype"
export const Talk = type({
title: "string",
track: "'ai' | 'web' | 'devops' | 'design'",
"coAuthor?": "string"
})ثلاثة أمور حدثت في ستة أسطر:
- تحوّل
"string"إلى مُدقّق نصوص في وقت التشغيل. - تحوّل
"'ai' | 'web' | 'devops' | 'design'"إلى اتحاد مُميَّز — تُصرِّف ArkType هذا إلى switch، لا إلى سلسلة مقارنات. - جعل
"coAuthor?"المفتاح اختيارياً. علامة?توضع على المفتاح لا على القيمة، تماماً كما في واجهة TypeScript.
قارن ذلك بالواجهة المكافئة التي كنت ستكتبها على أي حال:
interface Talk {
title: string
track: "ai" | "web" | "devops" | "design"
coAuthor?: string
}الشكلان متطابقان تقريباً. وهذا هو جوهر الفكرة كلها.
الخطوة 3: التحقق ومعالجة الأخطاء
أنواع ArkType قابلة للاستدعاء. استدعِ نوعاً ببيانات مجهولة فتحصل إما على القيمة المُتحقَّق منها أو على كائن أخطاء:
import { type } from "arktype"
import { Talk } from "@/lib/schemas/talk"
const out = Talk({
title: "Shipping AI Agents",
track: "quantum"
})
if (out instanceof type.errors) {
console.error(out.summary)
// track must be "ai", "web", "devops" or "design" (was "quantum")
} else {
console.log(out.title) // مُصنّف بالكامل كـ string
}لا يوجد انقسام بين .parse() و .safeParse(). موضع استدعاء واحد، وتفرّع واحد. وفحص instanceof type.errors هو حارس نوع في TypeScript، لذا يضيق نوع out داخل فرع else تلقائياً إلى النوع المُتحقَّق منه.
وحين تريد السلوك الذي يرمي استثناءً — تحميل الإعدادات، الاختبارات، الاستدعاءات الداخلية الموثوقة — استخدم .assert():
const talk = Talk.assert({ title: "Shipping AI Agents", track: "ai" })
// يرمي TraversalError إن كان غير صالح، ويعيد القيمة المُصنّفة خلاف ذلكولأخطاء الواجهة على مستوى الحقل، مُرّ على مصفوفة الأخطاء بدل قراءة الملخص:
if (out instanceof type.errors) {
for (const error of out) {
console.log(error.path, error.message)
// ["track"] must be "ai", "web", "devops" or "design" (was "quantum")
}
}out.summary نص متعدد الأسطر مقروء للبشر، مثالي للسجلات ومخرجات سطر الأوامر. أما الصيغة القابلة للتكرار مع error.path فهي ما تريده للنماذج، لأنها تخبرك أي حقل تُبرزه.
الخطوة 4: استنتاج أنواع TypeScript
لا تكتب الواجهة يدوياً أبداً. استنتجها:
export type Talk = typeof Talk.inferلديك الآن قيمة اسمها Talk ونوع اسمه Talk في الوحدة نفسها — إذ يفصل TypeScript بين فضاءي أسماء القيم والأنواع، فهذا قانوني ومتّبع في قواعد كود ArkType.
وحين يحتوي المخطط على morphs (الخطوة 6)، يفترق نوع الدخل عن نوع الخرج. تُتيح ArkType كليهما:
type TalkInput = typeof Talk.inferIn // ما تمرّره
type TalkOutput = typeof Talk.infer // ما تستعيدهاستخدم inferIn لحالة النموذج وأنواع طلبات الـ API، واستخدم infer لكل ما يأتي بعد التحقق.
الخطوة 5: القيود والكلمات المفتاحية المدمجة
نادراً ما يكفي "string" المجرّد. تأتي ArkType بمكتبة كلمات مفتاحية وصياغة قيود، وكلاهما يعيش داخل نص التعريف.
وسّع الملف lib/schemas/talk.ts:
import { type } from "arktype"
export const Talk = type({
// قيود الطول تُقرأ كمقارنات
title: "5 <= string <= 120",
abstract: "string >= 200",
// كلمات مفتاحية منقوطة للصيغ الشائعة
email: "string.email",
slidesUrl: "string.url",
submissionId: "string.uuid",
// قيود عددية وقابلية القسمة
durationMinutes: "number.integer >= 15",
seatBlock: "number % 5",
track: "'ai' | 'web' | 'devops' | 'design'",
// المصفوفات لاحقة، تماماً كما في TypeScript
tags: "string[]",
// ولها حدود طول خاصة بها
speakers: "string.email[] >= 1"
})جولة سريعة على ما يجري:
| الصياغة | المعنى |
|---|---|
5 <= string <= 120 | طول النص بين 5 و120 شاملاً الطرفين |
string >= 200 | طول أدنى 200 |
number.integer >= 15 | عدد صحيح لا يقل عن 15 |
number % 5 | قابل للقسمة على 5 |
string[] | مصفوفة نصوص |
string.email[] >= 1 | مصفوفة غير فارغة من نصوص البريد الإلكتروني |
أضافت ArkType 2.2 أيضاً string.hex و string.regex إلى مجموعة الكلمات المفتاحية، إلى جانب الموجود سلفاً مثل string.date و string.json و string.semver و string.ip و number.epoch.
تستخدم التقاطعات علامة &، فيمكنك دمج كلمة مفتاحية مع نمط:
const CompanyEmail = type("string.email & /@noqta\\.tn$/")داخل نص TypeScript حرفي، يجب مضاعفة كل شرطة مائلة عكسية في الـ regex. فيصبح /\d{2}/ هو "/\\d{2}/". وإن نسيت ذلك ستحصل على خطأ تحليل مربك يشير إلى الحرف الخطأ.
الخطوة 6: الـ Morphs — حوّل ولا تكتفِ بالتحقق
يصل دخل الويب على شكل نصوص. والمُدقّق الذي يقول فقط "هذا نص رقمي صالح" يتركك تستدعي Number() يدوياً بعدها، خارج نظام الأنواع. الـ morphs تسدّ هذه الفجوة.
الـ morph دالة تُطبَّق بعد التحقق، ويتدفق نوع إرجاعها إلى نوع الخرج المُستنتَج. أنشئ lib/schemas/form.ts:
import { type } from "arktype"
// صيغة الصفوف: [الدخل، "=>"، التحويل]
const TrimmedString = type("string", "=>", (s) => s.trim())
// عامل الأنبوب يسلسل: تحقق -> تحويل -> تحقق
const PositiveIntFromString = type("string.integer.parse |> number > 0")
const Slug = type("string", "=>", (s) =>
s
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
)
export const TalkFormInput = type({
title: TrimmedString,
durationMinutes: PositiveIntFromString,
slug: Slug
})شغّله:
const out = TalkFormInput({
title: " Shipping AI Agents ",
durationMinutes: "45",
slug: "Shipping AI Agents!"
})
// out.title -> "Shipping AI Agents" (string)
// out.durationMinutes -> 45 (number وليس string)
// out.slug -> "shipping-ai-agents" (string)لاحظ الأنواع المُستنتَجة. durationMinutes نوعه number عند الخروج و string عند الدخول — فـ typeof TalkFormInput.inferIn و typeof TalkFormInput.infer مختلفان، وكلاهما صحيح.
صار عامل الأنبوب |> قابلاً للتضمين داخل نصوص التعريف في 2.2، ويمنحك type.pipe() صيغة الدالة متعددة المعاملات حين تطول السلسلة:
const TrimToNonEmpty = type.pipe(
type.string,
(s) => s.trimStart(),
type.string.atLeastLength(1)
)يمكن للـ morphs أن تفشل أيضاً. أعِد ctx.error(...) لرفض القيمة داخل التحويل:
const IsoDate = type("string", "=>", (s, ctx) => {
const date = new Date(s)
return Number.isNaN(date.valueOf())
? ctx.error("a valid ISO 8601 date")
: date
})الآن يُستنتَج IsoDate كـ Date ويرفض المدخلات الفاسدة بخطأ ArkType سليم بدل رمي استثناء.
الـ morphs هي سبب سهولة مبدأ "حوّل ولا تتحقق فقط" في ArkType. ادفع كل تحويل من نص إلى كائن مجال داخل المخطط، فلا ترى بقية قاعدة الكود قيمة غير مُحوَّلة أبداً.
الخطوة 7: regex مُصنّف الأنواع مع مجموعات الالتقاط
هذه الميزة الأبرز في ArkType 2.2، ولا مقابل لها في بقية المُدقّقات الكبرى.
ضع البادئة x/ قبل نص الـ regex فتُحلّل ArkType النمط على مستوى الأنواع، مانحةً إياك مجموعات التقاط مُسمّاة ومُصنّفة على الخرج المُتحقَّق منه:
import { type } from "arktype"
export const SessionCode = type({
code: "x/^(?<year>\\d{4})-(?<track>[A-Z]{3})-(?<seq>\\d{3})$/"
})
const data = SessionCode.assert({ code: "2026-WEB-014" })
data.code.groups.year // "2026" — مُصنّف كـ string، مع إكمال تلقائي
data.code.groups.track // "WEB"
data.code.groups.seq // "014"مرّر المؤشر فوق groups في محررك: يعرف TypeScript أسماء المفاتيح بدقة، لأن المُصرِّف حلّل النمط. غيّر اسم مجموعة التقاط في الـ regex فينكسر كل مستهلك لها وقت التصريف. واكتب data.code.groups.yaer خطأً فتحصل على خطأ، لا على undefined في الثالثة فجراً.
تأتي الآلية الأساسية باسم ArkRegex، وهي بديل مباشر لـ RegExp باستنتاج أنواع كامل ودون أي كلفة في وقت التشغيل — إذ يجري التحليل كلياً داخل نظام الأنواع.
اجمعها مع morph لاستخراج بيانات مهيكلة في تمريرة واحدة:
export const ParsedSessionCode = type(
"x/^(?<year>\\d{4})-(?<track>[A-Z]{3})-(?<seq>\\d{3})$/",
"=>",
(code) => ({
year: Number(code.groups.year),
track: code.groups.track,
sequence: Number(code.groups.seq)
})
)تعريف واحد صار يتحقق من الصيغة ويستخرج الأجزاء ويعيد كائناً مُصنّفاً.
الخطوة 8: القيم الافتراضية والـ Narrows والعلامات
القيم الافتراضية تستخدم = في التعريف. المفتاح ذو القيمة الافتراضية اختياري عند الدخل ومطلوب عند الخرج:
const TalkSettings = type({
isRecorded: "boolean = true",
maxAttendees: "number.integer = 100",
track: "'ai' | 'web' = 'web'"
})
TalkSettings({}) // { isRecorded: true, maxAttendees: 100, track: "web" }وللقيم الافتراضية غير البدائية، مرّر دالة مصنع حتى لا تتشارك النسخ:
const Draft = type({
tags: type("string[]").default(() => [])
})الـ Narrows تُلحق مُسنِدات مخصصة تعمل بعد التحقق البنيوي. استخدم صيغة الصفوف بـ ::
const EventWindow = type({
startsAt: "string.date.parse",
endsAt: "string.date.parse"
}).narrow((window, ctx) =>
window.endsAt > window.startsAt ||
ctx.mustBe("an end date after the start date")
)يستقبل الـ narrow الكائن بعد التحويل الكامل، فأنت تقارن قيم Date حقيقية لا نصوصاً.
العلامات (Brands) تجعل النوع اسمياً دون تغيير سلوك وقت التشغيل، باستخدام #:
const SeatCount = type("number % 5#seatBlock")
type SeatCount = typeof SeatCount.infer
// لن يعود العدد `number` العادي قابلاً للإسناد إلى SeatCountالآن لا تستطيع دالة تأخذ SeatCount أن تقبل صامتةً أي رقم صادفها — بل يجب أن يخرج من المُدقّق.
الخطوة 9: النطاقات لوحدات مخططات قابلة لإعادة الاستخدام
ما إن يتجاوز عدد أنواعك المترابطة حفنة قليلة حتى تتفوق الأسماء المستعارة النصية على الاستيرادات. النطاق فضاء أسماء من التعريفات يمكنها الإشارة إلى بعضها بالاسم:
import { scope } from "arktype"
export const conference = scope({
// الأسماء المستعارة: تعريفات مجردة، غير مغلّفة بـ type()
TalkId: "string.uuid",
Track: "'ai' | 'web' | 'devops' | 'design'",
Speaker: {
email: "string.email",
name: "5 <= string <= 80"
},
Talk: {
id: "TalkId",
title: "5 <= string <= 120",
track: "Track",
speakers: "Speaker[] >= 1",
"relatedTalks?": "TalkId[]"
}
})
export const schemas = conference.export()استخدم الوحدة المُصدَّرة في أي مكان:
import { schemas } from "@/lib/schemas/conference"
const out = schemas.Talk(payload)
type Talk = typeof schemas.Talk.inferأكثر أخطاء ArkType شيوعاً على الإطلاق: تغليف أسماء النطاق المستعارة بـ type(). داخل scope({...}) اكتب Track: "'ai' | 'web'" ولا تكتب أبداً Track: type("'ai' | 'web'"). النسخة المغلّفة تُصرَّف لكنها تكسر حل الأسماء المستعارة لكل ما يشير إليها.
تدعم النطاقات أيضاً العودية عبر this وتوقيعات الفهرسة:
export const treeScope = scope({
Category: {
name: "string",
"children?": "Category[]"
},
TalksById: {
"[string.uuid]": "Talk | undefined"
}
})الخطوة 10: الدوال المُتحقَّق منها عبر type.fn
قدّمت ArkType 2.2 الأداة type.fn التي تغلّف دالة بحيث تُتحقَّق معاملاتها وقيمة إرجاعها في وقت التشغيل:
import { type } from "arktype"
export const scoreTalk = type.fn(
"string >= 10", // الملخص
"number.integer >= 1", // عدد المُراجِعين
":", // الفاصل قبل نوع الإرجاع
"0 <= number <= 100"
)((abstract, reviewers) => {
const raw = Math.min(abstract.length / 10, 100)
return Math.round(raw / reviewers) * reviewers
})
scoreTalk("A long enough abstract about agents", 3) // number
scoreTalk("short", 3) // TraversalError: must be at least length 10تدعم المعاملات القيم الافتراضية والاختيارية والمتغيرة العدد بالصياغة نفسها التي تستخدمها في مخططات الكائنات:
const notify = type.fn(
"string.email",
"string = 'Your talk was received'"
)((to, message) => `${to}: ${message}`)
notify("speaker@noqta.tn") // "speaker@noqta.tn: Your talk was received"هذا مفيد فعلاً عند حدود الثقة: معالجات الـ webhooks، ونقاط دخول الإضافات، وكل ما يمكن أن يصل إليه استدعاء أداة من نموذج لغوي.
الخطوة 11: Server Action مُتحقَّق منه في Next.js
الآن اربط كل شيء. أنشئ app/submit/actions.ts:
"use server"
import { type } from "arktype"
const Submission = type({
title: "5 <= string <= 120",
abstract: "string >= 200",
email: "string.email",
track: "'ai' | 'web' | 'devops' | 'design'",
durationMinutes: "string.integer.parse |> 15 <= number <= 90",
"coAuthor?": "string.email"
})
export type SubmissionState = {
ok: boolean
message?: string
fieldErrors?: Record<string, string>
}
export async function submitTalk(
_prev: SubmissionState,
formData: FormData
): Promise<SubmissionState> {
const result = Submission(Object.fromEntries(formData))
if (result instanceof type.errors) {
const fieldErrors: Record<string, string> = {}
for (const error of result) {
const key = String(error.path[0] ?? "form")
fieldErrors[key] ??= error.message
}
return { ok: false, message: "Please fix the highlighted fields.", fieldErrors }
}
// result.durationMinutes رقم هنا، وقد فُحص مداه سلفاً
await saveTalk(result)
return { ok: true, message: "Submission received." }
}يعطيك Object.fromEntries(formData) كائناً من النصوص. أما الـ morph في string.integer.parse |> 15 <= number <= 90 فيحوّل durationMinutes ويفحص مداه داخل المخطط، فتستقبل saveTalk رقماً حقيقياً مضموناً ضمن المدى.
يستهلكه مكوّن العميل عبر useActionState:
"use client"
import { useActionState } from "react"
import { submitTalk, type SubmissionState } from "./actions"
const initial: SubmissionState = { ok: false }
export function TalkForm() {
const [state, action, pending] = useActionState(submitTalk, initial)
return (
<form action={action} className="space-y-4">
<label className="block">
<span>Title</span>
<input name="title" className="input" />
{state.fieldErrors?.title && (
<p className="text-red-500 text-sm">{state.fieldErrors.title}</p>
)}
</label>
<label className="block">
<span>Duration (minutes)</span>
<input name="durationMinutes" type="number" className="input" />
{state.fieldErrors?.durationMinutes && (
<p className="text-red-500 text-sm">
{state.fieldErrors.durationMinutes}
</p>
)}
</label>
<button disabled={pending}>
{pending ? "Submitting…" : "Submit talk"}
</button>
{state.message && <p>{state.message}</p>}
</form>
)
}تحقّق من جانب العميل عبر Standard Schema
تُطبّق ArkType معيار Standard Schema، الواجهة المشتركة المعتمدة عبر منظومة التحقق. هذا يعني أن React Hook Form يقبل نوع ArkType مباشرةً عبر المُحوّل القياسي — دون أي مُهايئ خاص بـ ArkType:
import { useForm } from "react-hook-form"
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
import { Submission } from "@/lib/schemas/submission"
const form = useForm({
resolver: standardSchemaResolver(Submission)
})ويعمل Standard Schema في الاتجاه المعاكس أيضاً: تستطيع ArkType 2.2 تضمين مُدقّقات من أي مكتبة متوافقة داخل تعريفاتها الخاصة. لذا فترحيل قاعدة كود كبيرة مخططاً تلو الآخر ليس حدثاً يُذكر:
import { type } from "arktype"
import { ZodAddress } from "./legacy/address" // ما يزال مخطط Zod
const Attendee = type({
name: "string",
address: ZodAddress // يعمل كما هو
})الخطوة 12: متغيرات البيئة و JSON Schema
تحقّق من متغيرات البيئة مرة واحدة عند تحميل الوحدة، حتى يتعطل التطبيق عند الإقلاع بسبب مفتاح ناقص بدل أن يتعطل أثناء طلب عميل. أنشئ lib/env.ts:
import { type } from "arktype"
const Env = type({
NODE_ENV: "'development' | 'test' | 'production'",
DATABASE_URL: "string.url",
RESEND_API_KEY: "string >= 20",
"SENTRY_DSN?": "string.url",
MAX_UPLOAD_MB: "string.integer.parse |> 1 <= number <= 50"
})
export const env = Env.assert(process.env)استورد env بدل لمس process.env في أي مكان آخر. ويكون MAX_UPLOAD_MB رقماً مُتحقَّقاً منه قبل أن يقرأه أي كود.
ولواجهة برمجية عامة، ولّد JSON Schema من التعريف نفسه باستخدام @ark/json-schema:
import { Submission } from "@/lib/schemas/submission"
export function GET() {
return Response.json(Submission.toJsonSchema())
}التحويل ثنائي الاتجاه — إذ يمكنك أيضاً تحليل JSON Schema قائم إلى نوع ArkType، مع بدائل قابلة للضبط للتراكيب التي يعبّر عنها JSON Schema ولا تعبّر عنها ArkType. وهذا يجعل تبنّي ArkType عملياً خلف عقد OpenAPI لا تتحكم فيه.
اختبار تنفيذك
أضف Vitest واختبر الفرعين معاً:
npm install -D vitest// lib/schemas/talk.test.ts
import { describe, it, expect } from "vitest"
import { type } from "arktype"
import { Submission } from "./submission"
const valid = {
title: "Shipping AI Agents in Production",
abstract: "x".repeat(250),
email: "speaker@noqta.tn",
track: "ai",
durationMinutes: "45"
}
describe("Submission", () => {
it("parses duration into a number", () => {
const out = Submission(valid)
expect(out).not.toBeInstanceOf(type.errors)
if (!(out instanceof type.errors)) {
expect(out.durationMinutes).toBe(45)
expect(typeof out.durationMinutes).toBe("number")
}
})
it("rejects an unknown track with a field path", () => {
const out = Submission({ ...valid, track: "quantum" })
expect(out).toBeInstanceOf(type.errors)
if (out instanceof type.errors) {
expect([...out][0].path).toEqual(["track"])
}
})
it("enforces the duration ceiling", () => {
const out = Submission({ ...valid, durationMinutes: "500" })
expect(out).toBeInstanceOf(type.errors)
})
})شغّل npx vitest run. ثلاثة اختبارات خضراء تؤكد الـ morph والاتحاد وحد المدى.
وعادة مفيدة: أضف تأكيداً على مستوى الأنواع حتى ينكسر البناء عند انحراف المخطط.
import type { Equals } from "arktype/internal/utils"
type Out = typeof Submission.infer
const _check: Equals<Out["durationMinutes"], number> = trueحل المشكلات
"Type instantiation is excessively deep" — عادةً بسبب اتحاد ضخم جداً أو نطاق عميق التكرار. قسّم المخطط إلى نطاق بأسماء مستعارة؛ فـ ArkType تُخزّن حل الأسماء المستعارة مؤقتاً وينخفض ضغط العمق.
الإكمال التلقائي بطيء في ملف مخطط كبير — رقِّ إلى TypeScript 5.9+ وتأكد أن skipLibCheck مضبوط على true. فاستنتاج ArkType ثقيل بحكم التصميم، والمُصرِّفات الأقدم تدفع ثمنه.
الـ regex لا يطابق أبداً — شبه مؤكد أنك استخدمت شرطة عكسية مفردة داخل نص حرفي. فـ "/\d+/" خطأ، و "/\\d+/" صواب.
قيمة groups هي undefined عند مطابقة regex — استخدمت نصاً حرفياً عادياً /.../ بدل البادئة x/.../. فصيغة x/ وحدها هي التي تشارك في التحليل على مستوى الأنواع.
اسم مستعار في نطاق يُحلّ إلى unknown — الاسم المستعار مغلّف بـ type(). أزل التغليف.
القيمة الافتراضية مُتشارَكة بين الكائنات — مرّرت مصفوفة أو كائناً حرفياً كقيمة افتراضية. مرّر دالة مصنع بدلاً منها: .default(() => []).
Server Action يرفض حقلاً رقمياً دائماً — تذكّر أن قيم FormData نصوص. استخدم string.integer.parse أو string.numeric.parse بدل number.
الخطوات التالية
- قارن المقاربات مع دليل التحقق من المخططات بـ Zod v4 ودرس التحقق المعياري بـ Valibot — الثلاثة تُطبّق Standard Schema، لذا فهي متوافقة فيما بينها.
- مرّر مخرجات JSON Schema من ArkType إلى مخرجات النماذج اللغوية المهيكلة؛ راجع BAML لمخرجات LLM مهيكلة وآمنة الأنواع.
- اجمع النطاقات مع مسارات API آمنة الأنواع عبر oRPC للحصول على عقود مُتحقَّق منها على طرفَي الاتصال.
- غلّف معالجات أدوات النماذج اللغوية بـ
type.fnحتى يُرفض أي معامل مُهلوَس قبل أن يصل إلى قاعدة بياناتك.
الخاتمة
طرح ArkType ضيّق ومحدد: يجب أن يشبه مُدقّقك أنواعك، لأنه هو أنواعك. وعملياً يُترجَم ذلك إلى أربعة مكاسب ملموسة.
تكتب أقل. فـ "5 <= string <= 120" تحل محل سلسلة بنّاء كاملة، والتعريف يُقرأ كما تصف القيد بصوتك.
وتلتقط أكثر أثناء الكتابة. فنصوص التعريف يفحصها مُصرِّف TypeScript، لذا يصبح الخطأ المطبعي في مخطط خطاً أحمر لا مفاجأة في وقت التشغيل. ومجموعات التقاط regex المُصنّفة تمدّ هذا الضمان إلى أرض لا يبلغها أي مُدقّق آخر.
وتحوّل بدل أن تتحقق فقط. فالـ morphs تدفع نصوص FormData وتواريخ ISO ومتغيرات البيئة الرقمية إلى قيم مجال حقيقية عند الحدود، فلا يتعامل أي شيء لاحق مع شكل غير مُحوَّل.
ولست محتجزاً. فـ Standard Schema يعني أن ArkType تتركّب مع مخططات Zod و Valibot في الاتجاهين، ويجسر @ark/json-schema إلى عالم OpenAPI الأوسع — فيصبح التبنّي تدريجياً، مخططاً تلو الآخر، بدءاً بذلك النموذج الواحد الذي يزعجك اليوم أكثر من غيره.