JSON به TypeScript: interfaceها، type guardها و محدودیتهای تولید
جمعبندی: تولید interface از JSON نمونه نیمی از کار است: فیلدهای اختیاری، null، unionها، تاریخها و عددهای صحیح بزرگ به بازبینی انسانی نیاز دارند و نوعها هیچ اعتبارسنجی زمان اجرا انجام نمیدهند.

جمعبندی: interface را برای صرفهجویی در تایپ تولید کنید، سپس اختیاریبودن و null را دوباره بررسی کنید، تاریخها و عددهای صحیح بزرگ را رشته نگه دارید و در مرز اعتبارسنجی زمان اجرا بیفزایید؛ رفتار کردن نوعهای تولیدشده بهعنوان قرارداد، هیچ اعتبارسنجیای به شما نمیدهد.
تولیدکننده فقط همان یک نمونهای را میبیند که در آن میچسبانید. اینکه فیلدها میتوانند غایب باشند، مقدارها میتوانند null باشند و تاریخها چه قالبی دارند، همگی حدس است — و اصلاح حدسهای اشتباه با شماست.
۱. تولیدکننده واقعاً چه میکند
یک نمونه JSON را در JSON به TypeScript بچسبانید و interface بازگشتی یک خلاصه شکل است: کلیدها و نوع مقدارهایی را که دیده میشمارد و سپس تنگترین نمایش ممکن را انتخاب میکند.
{
"id": 1,
"email": "ada@example.com",
"profile": { "nickname": "Ada", "age": 36 },
"tags": ["admin", "beta"],
"createdAt": "2026-10-31T08:00:00Z"
}
خروجی تقریباً چنین است:
interface User {
id: number;
email: string;
profile: { nickname: string; age: number };
tags: string[];
createdAt: string;
}
این کامپایل میشود، اما یک مقدار ایدهآل را توصیف میکند نه قرارداد واقعی endpoint. تولیدکننده نمیداند آیا email میتواند غایب باشد، آیا tags میتواند خالی باشد یا آیا createdAt همیشه منطقه زمانی دارد. حتی نمیداند id در رکوردهای دیگر ممکن است رشته باشد.
پس انتظار درست این است: تولیدکننده در تایپ صرفهجویی میکند، انسان معنا را تأمین میکند.
۲. فیلدهای اختیاری و null: ? همان | null نیست
این بخشی از خروجی تولیدشده است که بیش از همه کپی میشود و بیش از همه میشکند.
| شکل TypeScript | معنا | JSON متناظر |
|---|---|---|
email: string |
همیشه حاضر، همیشه رشته | {"email":"a@b.c"} |
email?: string |
کلید میتواند غایب باشد؛ وقتی هست نمیتواند null باشد | {} یا {"email":"a@b.c"} |
email: string | null |
کلید همیشه حاضر است، مقدار میتواند null باشد | {"email":null} |
email?: string | null |
کلید میتواند غایب باشد و مقدار هم میتواند null باشد | هر سه حالت |
تولیدکننده یک نمونه دیده، پس معمولاً شکل اول را بیرون میدهد. اگر API واقعاً میتواند null برگرداند، نوع دروغ میگوید: همه چیز کامپایل میشود و سپس email.toLowerCase() در زمان اجرا پرتاب میکند.
دو راه برای اصلاح:
۱. چند نمونه واقعی جمع کنید که هم «کلید غایب» و هم «مقدار null» را پوشش دهند، سپس دوباره تولید کنید.
۲. پس از تولید، فیلدها را دستی به ?: T | null تغییر دهید و هر دو شاخه را واقعاً در کد مدیریت کنید.
راه کوتاه «همه چیز را ?: string علامت زدن» را انتخاب نکنید. اختیاری یعنی «کلید ممکن است وجود نداشته باشد»، نه «مقدار ممکن است خالی باشد». undefined و null در JSON.stringify هم رفتار متفاوتی دارند: اولی حذف میشود، دومی بهصورت null میماند.
۳. عناصر ناهمگون آرایه و نوعهای union
آرایهها آسانترین ساختار برای اشتباه گرفتناند. اگر tags در سه رکورد بهصورت ["admin"]، [] و ["beta", 3] ظاهر شود، تولیدکننده بسته به نمونههایی که دیده ممکن است string[]، (string | number)[] یا unknown[] بدهد.
یک انسان باید تصمیم بگیرد:
- نوع عناصر واقعاً مختلط است:
(string | number)[]بنویسید و در محل استفاده narrow کنید. - فقط یک رکورد کثیف است: داده را اصلاح کنید. آن را با
any[]پنهان نکنید. - آرایههای خالی: تولیدکننده نمیتواند نوع عنصر را استنتاج کند و به
never[]یاany[]برمیگردد، پس باید خودتان پرش کنید. - تاپلهای طول ثابت: فقط وقتی از
[string, string]استفاده کنید که کسبوکار واقعاً طول را تضمین میکند؛ وگرنه افزودن یک عنصر بعداً خطای کامپایل میشود.
آرایههای object هم وقتی کلیدها ناهمگوناند نیاز به توجه دارند:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // some records do not carry this field
}
اختیاریبودن را روی نوع عنصر بگذارید، نه روی آرایه. items?: OrderItem[] یعنی خود آرایه میتواند غایب باشد، که بیانی کاملاً متفاوت است.
۴. تاریخها، عددهای صحیح بزرگ، enumها: نوعهایی که JSON ندارد
JSON فقط رشته و عدد دارد؛ معنای کسبوکار در قراردادها زندگی میکند. تولیدکننده از لیترالها استنتاج میکند، پس دستی اصلاحش کنید:
| لیترال JSON | خروجی معمول تولیدشده | آنچه باید باشد |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
در لایه انتقال string نگه دارید و در محل استفاده تبدیل کنید؛ هرگز مستقیم Date اعلام نکنید |
1717075200000 |
number |
برای timestamp میلیثانیهای از number استفاده کنید و واحد را مستند کنید |
"1234567890123456789" |
string |
آن را string نگه دارید تا number دقت را از دست ندهد |
1 و "1" مختلط |
number | string |
به یک شکل نرمال کنید؛ union را به کد کسبوکار نفرستید |
"admin" |
string |
یک union لیترال 'admin' | 'editor' یا ثابت enum مشترک |
true و "true" مختلط |
boolean | string |
منبع داده را اصلاح کنید نه اینکه چنین نوعی تولید کنید |
مهمترین قاعده: تاریخها در JSON همیشه رشتهاند. نوشتن createdAt: Date یک خودفریبی رایج است — JSON.parse هرگز Date نمیسازد، پس مقدار زمان اجرا همچنان رشته است و .getTime() پرتاب میکند. نوع انتقال را string نگه دارید، در مرز صریحاً new Date(value) را صدا بزنید و Number.isNaN را بررسی کنید.
عددهای صحیح بزرگ هم همینطورند. وقتی id از 2^53 فراتر میرود، JSON.parse از پیش دقت را از دست داده و تولید number فقط باگ را در نوع تثبیت میکند. بکاند رشته برمیگرداند، فرانتاند رشته نگه میدارد؛ این تنها گزینه قابل اعتماد است.
۵. نوعهای تولیدشده هیچ اعتبارسنجی زمان اجرا انجام نمیدهند
نوعهای TypeScript در زمان کامپایل پاک میشوند. هرقدر interface دقیق باشد، نمیتواند پاسخی مانند {"id": "x"} را متوقف کند — کد خودتان را محدود میکند، نه بایتهایی را که از شبکه میرسند.
/**
* Check whether a runtime value is a user object.
* @param value The runtime value to validate
* @returns Whether it satisfies the minimum shape of User
*/
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as Record<string, unknown>;
return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}
یک type guard فقط فیلدهایی را پوشش میدهد که در بررسی مینویسید. با زیاد شدن فیلدها، guardهای دستساز به تعریف دومی برای نگهداری تبدیل میشوند که از interface دور میافتد.
مقایسه عملی چنین است:
| رویکرد | اعتبارسنجی زمان اجرا | منبع نوعها | مناسب |
|---|---|---|---|
| فقط interface | هیچ | دستنویس یا تولیدشده | داده داخلی قابل اعتماد، قیدهای زمان کامپایل |
| interface بههمراه guard دستساز | بله | دو تعریف که واگرا میشوند | فیلدهای کم، مرز روشن |
| zod / valibot | بله | نوعها از schema استنتاج میشوند | APIهای خارجی، فرمها، پیکربندی |
| JSON Schema بههمراه اعتبارسنج | بله | نوعها از schema تولید میشوند | قراردادهای چندزبانه، مستندسازی |
اگر داده از بیرون میآید، schema را تنها منبع حقیقت کنید و نوعها را با z.infer یا json-schema-to-ts مشتق کنید. آنگاه «اعتبارسنجی عبور کرد» و «نوع درست است» یک واقعیت واحدند و تعریف دومی برای واگرایی وجود ندارد. برای رویکرد کامل همگامسازی دوسویه، همگامسازی Schema و نوع TypeScript را ببینید.
۶. interface یا type: بر سر خروجی تولیدشده بحث نکنید
تولیدکنندهها معمولاً interface میدهند، اما این دو در بیشتر موقعیتها قابل تبادلاند. آنچه واقعاً اهمیت دارد سه قابلیت است:
- ادغام اعلان (declaration merging):
interfaceرا میتوان چند بار اعلان کرد و خودکار ادغام میشود؛typeنه. این هنگام وصله کردن نوعهای شخص ثالث کمک میکند، اما مدلهای کسبوکار باید از آن پرهیز کنند — ادغام اجازه میدهد «فیلدهای اضافه» بیآنکه کامپایلر متوجه شود رد شوند. - unionها و mapped typeها:
typeمیتواندA | B،Partial<T>و template literal type را مستقیم بیان کند؛interfaceنمیتواند. - بازگشتی: هر دو پشتیبانی میکنند، اما شکلهای بازگشتی پیچیده بهصورت
typeطبیعیتر خوانده میشوند.
قاعده مفیدتر برای تیم: interface برای شکل object، type برای unionها، نوعهای کمکی و نوعهای تابع. خروجی تولیدشده را در پیشفرضش رها کنید و زمان بازبینی را صرف اختیاریبودن و اعتبارسنجی زمان اجرا کنید.
۷. زنجیره ابزار: سه راه تولید
| رویکرد | ورودی | قوت | محدودیت |
|---|---|---|---|
| JSON به TypeScript سایت | JSON نمونه | بیدرنگ کار میکند، پردازش محلی | فقط بر اساس نمونه فعلی؛ نیاز به narrow کردن دستی |
| quicktype | JSON / JSON Schema | خروجی چندزبانه، ادغام CLI | استنتاج محافظهکارانه، unionها میتوانند بیشازحد پهن باشند |
| json-schema-to-typescript | JSON Schema | قراردادمحور و بازتولیدپذیر | نخست به schema قابل اعتماد نیاز دارد |
یک فراخوانی معمول quicktype:
npx quicktype -s json -o user.ts user.json --just-types
--just-types فقط نوع میدهد و کد تبدیل زمان اجرا نه، که برای پروژههایی با لایه تجزیه موجود مناسب است. اگر schema دستنویس یا تولید بکاند باشد، json-schema-to-typescript انتخاب بهتری است چون required، enum و additionalProperties را محترم میشمارد — قیدهایی که استنتاج از نمونه بهسادگی نمیبیند.
تولید schema تلههای خودش را دارد، مانند علامت زدن همه فیلدها بهعنوان required یا تبدیل بیصدای null به اختیاری؛ تلههای رایج تولیدکننده JSON Schema را ببینید.
۸. چکلیست پیش از commit نوعهای تولیدشده
پیش از commit خروجی این پنج مورد را مرور کنید:
۱. نام فیلدها را نرمال کنید: نگذارید userId و user_id همزمان وجود داشته باشند؛ یک بار در لایه تبدیل نگاشت کنید.
۲. اختیاریبودن را دوباره بررسی کنید: با پاسخهای واقعی مقایسه کنید و بیابید کدام فیلدها میتوانند غایب یا null باشند.
۳. تاریخها و idها را رشته نگه دارید: واحد و قالب را در نوع مستند کنید و در محل استفاده صریح تجزیه کنید.
۴. any و unknown را کم کنید: هرجا تولیدکننده any داد، یا نوع بیفزایید یا unknown نگه دارید و guard اضافه کنید.
۵. اعتبارسنجی زمان اجرا بیفزایید: داده خارجی را حداقل یک بار اعتبارسنجی کنید؛ از interface انتظار نداشته باشید داده بد را بگیرد.
اگر پروژه از چند کتابخانه JSON استفاده میکند (JSON.parse، superjson، lossless-json)، رفتارشان با عددهای صحیح بزرگ و تاریخها متفاوت است، پس هنگام انتخاب تأیید کنید. انتخاب کتابخانه تجزیه JSON را ببینید.
چرا تولیدکننده هر فیلد اختیاری را required علامت میزند؟
یک نمونه دیده، پس هر کلیدی که ظاهر شده همیشه حاضر فرض میشود. چند نمونه بدهید که حالتهای متفاوت را پوشش دهند، یا از JSON Schema تولید کنید — آرایه required آنجا منبع مرجع است.
چرا اعلام Date در interface در زمان اجرا شکست میخورد؟
JSON.parse فقط رشته، عدد، بولین، null، object و array تولید میکند؛ هرگز Date نمیسازد. اعلام Date یک وعده زمان کامپایل است در حالی که مقدار زمان اجرا رشته میماند. در لایه انتقال string نگه دارید و در مرز تبدیل کنید.
آیا نوعهای تولیدشده را میتوان بهعنوان اعتبارسنجی استفاده کرد؟
نه. نوعهای TypeScript پس از کامپایل پاک میشوند و هیچ چیزی را بر داده زمان اجرا تحمیل نمیکنند. داده خارجی را با zod، valibot، ajv یا یک type guard دستساز اعتبارسنجی کنید.
id عدد صحیح بزرگ را number تایپ کنم یا string؟
اگر میتواند از 2^53 فراتر رود، باید string باشد. JSON.parse هنگام تجزیه دقت را از دست میدهد و تایپ آن بهصورت number فقط باگ را در جایی از نوع مینویسد که دیگر قابل اصلاح نیست.