JSON থেকে TypeScript: ইন্টারফেস, টাইপ গার্ড ও জেনারেশনের সীমা
সংক্ষেপে: নমুনা JSON থেকে ইন্টারফেস জেনারেট করা অর্ধেক কাজ: ঐচ্ছিক ফিল্ড, null, ইউনিয়ন, তারিখ ও বড় ইন্টিজার মানুষের হাত পেতে হয়, আর টাইপ রানটাইমে কোনো যাচাই করে না।

মূল কথা: টাইপিং বাঁচাতে ইন্টারফেস জেনারেট করুন, তারপর ঐচ্ছিকতা ও null আবার যাচাই করুন, তারিখ ও বড় ইন্টিজার স্ট্রিং রাখুন, আর সীমানায় রানটাইম ভ্যালিডেশন যোগ করুন; জেনারেটেড টাইপকে চুক্তি ভাবলে আপনি কোনো ভ্যালিডেশনই পাবেন না।
জেনারেটর কেবল আপনি যে একটি নমুনা পেস্ট করেছেন সেটিই দেখে। ফিল্ড অনুপস্থিত হতে পারে কি না, মান null হতে পারে কি না, তারিখ কোন ফরম্যাটে — সবই অনুমান, আর ভুল অনুমান ঠিক করার দায় আপনার।
১. জেনারেটর আসলে কী করে
JSON থেকে TypeScript-এ একটি JSON নমুনা পেস্ট করুন, ফিরে পাওয়া ইন্টারফেস হলো একটি আকৃতির সারাংশ: এটি যত কি ও ভ্যালু-টাইপ দেখেছে তা গণনা করে, তারপর উপলব্ধ সবচেয়ে সংকীর্ণ রূপ বেছে নেয়।
{
"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;
}
এটি কম্পাইল হয়, কিন্তু এটি একটি আদর্শ মান বর্ণনা করে, এন্ডপয়েন্টের আসল চুক্তি নয়। জেনারেটর জানে না 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() throws করে।
ঠিক করার দুই উপায়:
- "কি অনুপস্থিত" ও "মান null" — দুই ক্ষেত্রেই কভার করে এমন একাধিক বাস্তব নমুনা সংগ্রহ করুন, তারপর জেনারেটরকে আবার অনুমান করতে দিন।
- জেনারেশনের পর হাতে ফিল্ডকে
?: T | nullকরুন এবং কোডে সত্যিই দুই শাখা হ্যান্ডল করুন।
সবকিছু ?: string করে দেওয়ার শর্টকাট নেবেন না। ঐচ্ছিক মানে "কি-টি নাও থাকতে পারে", "মান খালি হতে পারে" নয়। undefined ও null-এর আচরণও JSON.stringify-এ আলাদা: প্রথমটি বাদ পড়ে, দ্বিতীয়টি null হিসেবে থাকে।
৩. অসম উপাদান ও ইউনিয়ন টাইপ
অ্যারে ভুল করার সবচেয়ে সহজ কাঠামো। tags যদি তিন রেকর্ডে ["admin"], [] ও ["beta", 3] হিসেবে দেখা যায়, জেনারেটর যে নমুনা দেখেছে তার উপর নির্ভর করে string[], (string | number)[] বা unknown[] দিতে পারে।
মানুষকে ঠিক করতে হবে:
- উপাদানের টাইপ সত্যিই মিশ্র:
(string | number)[]লিখুন এবং ব্যবহারের জায়গায় সংকীর্ণ করুন। - কেবল একটি রেকর্ড নোংরা: ডেটা ঠিক করুন।
any[]দিয়ে ঢাকবেন না। - খালি অ্যারে: জেনারেটর উপাদানের টাইপ অনুমান করতে পারে না এবং
never[]বাany[]-এ পড়ে, তাই আপনাকে ভরতে হবে। - স্থির দৈর্ঘ্যের টাপল: কেবল যখন ব্যবসা সত্যিই দৈর্ঘ্য নিশ্চিত করে তখন
[string, string]; নাহলে পরে উপাদান যোগ করা কম্পাইল এরর হয়।
অবজেক্টের অ্যারেও কি-অসমতায় খেয়াল দরকার:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // কিছু রেকর্ডে এই ফিল্ড নেই
}
ঐচ্ছিকতা উপাদানের টাইপে দিন, অ্যারেতে নয়। items?: OrderItem[] মানে অ্যারেটি নিজেই অনুপস্থিত হতে পারে, যা সম্পূর্ণ আলাদা কথা।
৪. তারিখ, বড় ইন্টিজার, এনাম: JSON-এ নেই এমন টাইপ
JSON-এ কেবল স্ট্রিং ও সংখ্যা; ব্যবসায়িক অর্থ প্রথার মধ্যে থাকে। জেনারেটর লিটেরাল থেকে অনুমান করে, তাই হাতে সংশোধন করুন:
| JSON লিটেরাল | সাধারণ জেনারেটেড আউটপুট | যা হওয়া উচিত |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
পরিবহন স্তরে string রাখুন এবং ব্যবহারের জায়গায় রূপান্তর করুন; কখনো সরাসরি Date ঘোষণা করবেন না |
1717075200000 |
number |
মিলিসেকেন্ড টাইমস্ট্যাম্পে number ব্যবহার করুন এবং একক নথিভুক্ত করুন |
"1234567890123456789" |
string |
string রাখুন যাতে number নির্ভুলতা না হারায় |
1 ও "1" মিশ্র |
number | string |
এক রূপে নরমালাইজ করুন; ইউনিয়ন ব্যবসায়িক কোডে ঠেলবেন না |
"admin" |
string |
লিটেরাল ইউনিয়ন 'admin' | 'editor', বা শেয়ার্ড এনাম কনস্ট্যান্ট |
true ও "true" মিশ্র |
boolean | string |
এই টাইপ জেনারেট না করে ডেটা উৎস ঠিক করুন |
সবচেয়ে গুরুত্বপূর্ণ নিয়ম: JSON-এ তারিখ সবসময় স্ট্রিং। createdAt: Date লেখা একটি সাধারণ আত্মপ্রতারণা — JSON.parse কখনো Date বানায় না, তাই রানটাইম মান এখনো স্ট্রিং আর .getTime() throws করে। পরিবহন টাইপ string রাখুন, সীমানায় স্পষ্টভাবে new Date(value) কল করুন, আর Number.isNaN যাচাই করুন।
বড় ইন্টিজারও একই নিয়মে। id 2^53 ছাড়ালে JSON.parse ইতিমধ্যে নির্ভুলতা হারিয়েছে, আর number জেনারেট করলে বাগটি টাইপেই পাকা হয়ে যায়। ব্যাকএন্ড স্ট্রিং ফেরত দিক, ফ্রন্টএন্ড স্ট্রিং রাখুক; এটিই একমাত্র নির্ভরযোগ্য বিকল্প।
৫. জেনারেটেড টাইপ রানটাইমে কোনো ভ্যালিডেশন করে না
TypeScript টাইপ কম্পাইলে মুছে যায়। ইন্টারফেস যতই নিখুঁত হোক, {"id": "x"} রেসপন্স আটকাতে পারে না — এটি আপনার নিজের কোডকে বাঁধে, নেটওয়ার্ক দিয়ে আসা বাইটকে নয়।
/**
* রানটাইম মান ব্যবহারকারী অবজেক্ট কি না যাচাই করে।
* @param value যাচাই করার রানটাইম মান
* @returns 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';
}
টাইপ গার্ড কেবল আপনি যে ফিল্ড চেকে লিখেছেন তা-ই কভার করে। ফিল্ড বাড়লে হাতে লেখা গার্ড রক্ষণাবেক্ষণের দ্বিতীয় ডেফিনিশন হয়ে ওঠে, যা ইন্টারফেস থেকে সরে যায়।
ব্যবহারিক সমঝোতা এমন:
| পদ্ধতি | রানটাইম ভ্যালিডেশন | টাইপের উৎস | মানানসই |
|---|---|---|---|
| কেবল ইন্টারফেস | নেই | হাতে লেখা বা জেনারেটেড | বিশ্বস্ত অভ্যন্তরীণ ডেটা, কম্পাইল-সময়ের বাধ্যবাধকতা |
| ইন্টারফেস প্লাস হাতে লেখা গার্ড | আছে | দুই ডেফিনিশন যা সরে যায় | কম ফিল্ড, স্পষ্ট সীমানা |
| zod / valibot | আছে | স্কিমা থেকে অনুমিত টাইপ | বাহ্যিক API, ফর্ম, কনফিগ |
| JSON Schema প্লাস ভ্যালিডেটর | আছে | স্কিমা থেকে জেনারেটেড টাইপ | বহুভাষিক চুক্তি, ডকুমেন্টেশন |
ডেটা বাইরে থেকে এলে স্কিমাকে সত্যের একক উৎস বানান এবং z.infer বা json-schema-to-ts দিয়ে টাইপ বের করুন। তখন "ভ্যালিডেশন পাস" আর "টাইপ সঠিক" একই তথ্য, আর সরে যাওয়ার দ্বিতীয় ডেফিনিশন নেই। পূর্ণ দ্বিমুখী সমন্বয় পদ্ধতির জন্য দেখুন Schema ও TypeScript টাইপ সমন্বয়।
৬. Interface নাকি type: জেনারেটেড আউটপুট নিয়ে তর্ক করবেন না
জেনারেটর সাধারণত interface দেয়, তবে বেশিরভাগ পরিস্থিতিতে দুটি বিনিময়যোগ্য। আসলে গুরুত্বপূর্ণ তিনটি সামর্থ্য:
- ডিক্লারেশন মার্জিং:
interfaceএকাধিকবার ঘোষণা করা যায় এবং স্বয়ংক্রিয়ভাবে মার্জ হয়;typeপারে না। তৃতীয়-পক্ষের টাইপ প্যাচ করার সময় কাজে লাগে, তবে ব্যবসায়িক মডেলে এড়ানো উচিত — মার্জিং "বাড়তি ফিল্ড" কম্পাইলার চোখ এড়িয়ে যেতে দেয়। - ইউনিয়ন ও ম্যাপড টাইপ:
typeসরাসরিA | B,Partial<T>ও টেমপ্লেট লিটেরাল টাইপ প্রকাশ করতে পারে;interfaceপারে না। - রিকার্সন: দুটোই সমর্থন করে, তবে জটিল রিকার্সিভ আকৃতি
typeহিসেবে বেশি স্বাভাবিক পড়া যায়।
বেশি কাজে দেওয়া দলের নিয়ম: অবজেক্ট আকৃতিতে interface, ইউনিয়ন, ইউটিলিটি টাইপ ও ফাংশন টাইপে type। জেনারেটেড আউটপুট ডিফল্টে রাখুন আর রিভিউয়ের সময় ঐচ্ছিকতা ও রানটাইম ভ্যালিডেশনে দিন।
৭. টুলচেইন: জেনারেট করার তিনটি উপায়
| পদ্ধতি | ইনপুট | শক্তি | সীমাবদ্ধতা |
|---|---|---|---|
| সাইটের JSON থেকে TypeScript | নমুনা JSON | সঙ্গে সঙ্গে কাজ করে, স্থানীয়ভাবে প্রসেস হয় | কেবল বর্তমান নমুনার উপর ভিত্তি; হাতে সংকীর্ণ করা দরকার |
| quicktype | JSON / JSON Schema | বহুভাষিক আউটপুট, CLI সংযোগ | রক্ষণশীল অনুমান, ইউনিয়ন বেশি চওড়া হতে পারে |
| json-schema-to-typescript | JSON Schema | চুক্তি-চালিত ও পুনরুৎপাদনযোগ্য | আগে বিশ্বাসযোগ্য স্কিমা দরকার |
quicktype-এর একটি সাধারণ আহ্বান:
npx quicktype -s json -o user.ts user.json --just-types
--just-types কেবল টাইপ দেয়, রানটাইম রূপান্তর কোড নয়, যা এমন প্রকল্পে মানানসই যেখানে ইতিমধ্যে পার্সিং স্তর আছে। স্কিমা হাতে লেখা বা ব্যাকএন্ড থেকে আসা হলে json-schema-to-typescript বেশি উপযুক্ত, কারণ এটি required, enum ও additionalProperties মানে — যে বাধ্যবাধকতাগুলো নমুনা-অনুমান দেখতেই পায় না।
স্কিমা জেনারেশনের নিজস্ব ফাঁদ আছে, যেমন প্রতিটি ফিল্ড required করা বা নীরবে null-কে ঐচ্ছিক বানানো; দেখুন JSON Schema জেনারেটরের সাধারণ ফাঁদ।
৮. জেনারেটেড টাইপ কমিট করার আগে চেকলিস্ট
আউটপুট কমিট করার আগে এই পাঁচটি বিষয় চালিয়ে নিন:
- ফিল্ডের নাম নরমালাইজ করুন:
userIdওuser_idএকসঙ্গে থাকতে দেবেন না; রূপান্তর স্তরে একবার ম্যাপ করুন। - ঐচ্ছিকতা আবার যাচাই করুন: বাস্তব রেসপন্সের সঙ্গে মিলিয়ে দেখুন কোন ফিল্ড অনুপস্থিত বা
nullহতে পারে। - তারিখ ও id স্ট্রিং রাখুন: টাইপে একক ও ফরম্যাট নথিভুক্ত করুন, আর ব্যবহারের জায়গায় স্পষ্টভাবে পার্স করুন।
anyওunknownকমান: জেনারেটর যেখানেanyদিয়েছে সেখানে টাইপ যোগ করুন, বাunknownরাখুন ও গার্ড যোগ করুন।- রানটাইম ভ্যালিডেশন যোগ করুন: বাহ্যিক ডেটা অন্তত একবার যাচাই করুন; ইন্টারফেস খারাপ ডেটা ধরবে ভাববেন না।
প্রকল্পে একাধিক JSON লাইব্রেরি (JSON.parse, superjson, lossless-json) ব্যবহার হলে বড় ইন্টিজার ও তারিখে তাদের আচরণ আলাদা, তাই বাছাইয়ের সময় নিশ্চিত করুন। দেখুন JSON পার্সিং লাইব্রেরি বাছাই।
জেনারেটর প্রতিটি ঐচ্ছিক ফিল্ডকে required কেন করে?
এটি একটি মাত্র নমুনা দেখেছে, তাই যে কি দেখেছে তাকেই সবসময় উপস্থিত ধরে। ভিন্ন কেস কভার করে একাধিক নমুনা দিন, বা JSON Schema থেকে জেনারেট করুন — সেখানে required অ্যারে প্রামাণিক উৎস।
ইন্টারফেসে Date ঘোষণা করলে রানটাইমে ব্যর্থ হয় কেন?
JSON.parse কেবল স্ট্রিং, সংখ্যা, বুলিয়ান, null, অবজেক্ট ও অ্যারে তৈরি করে; কখনো Date বানায় না। Date ঘোষণা কম্পাইল-সময়ের প্রতিশ্রুতি, অথচ রানটাইম মান স্ট্রিং থাকে। পরিবহন স্তরে string রাখুন এবং সীমানায় রূপান্তর করুন।
জেনারেটেড টাইপ ভ্যালিডেশন হিসেবে ব্যবহার করা যায়?
না। TypeScript টাইপ কম্পাইলের পর মুছে যায় এবং রানটাইম ডেটার উপর কিছু আরোপ করে না। বাহ্যিক ডেটা zod, valibot, ajv বা হাতে লেখা টাইপ গার্ড দিয়ে যাচাই করুন।
বড় ইন্টিজার id-কে number নাকি string টাইপ করব?
2^53 ছাড়াতে পারে হলে অবশ্যই string। JSON.parse পার্সের সময় নির্ভুলতা হারায়, আর number টাইপ করলে বাগটি টাইপেই লিখে যায় যেখান থেকে আর ঠিক করা যায় না।