বেশিরভাগ মানুষ TypeScript শেখে একটা labeling system হিসেবে: কিছু ডেটা আছে, তার আকৃতি বর্ণনা করে একটা type লেখেন, editor autocomplete দেয়। এটা কাজের, কিন্তু type system কীসের জন্য তার ছোট অর্ধেক এটা। বড় অর্ধেকটা হলো type ব্যবহার করে কিছু ভুল state লিখে ফেলাই অসম্ভব করে দেওয়া — যাতে testing-এ ধরা পড়ত এমন বাগ হয়ে যায় একটা লাল দাগ যা পেরিয়ে ship করা যায় না।
এটা বোঝার পর, "এই ডেটা দেখতে কেমন?" জিজ্ঞেস করা বন্ধ করে "এই code-এর কখনোই কী অনুমতি দেওয়া উচিত না?" জিজ্ঞেস করা শুরু করেন। সেখানে পৌঁছানোর তিনটা পদক্ষেপ এখানে।
Boolean-এর জগাখিচুড়ি: যখন প্রতিটা flag আপনার খারাপ state দ্বিগুণ করে
এখানে একটা type যা আমি যতবার চাই তার চেয়ে বেশিবার লিখেছি ও review করেছি:
type RequestState = {
loading: boolean;
data?: User;
error?: string;
};যুক্তিসঙ্গত দেখায়। এটা আটটা সম্ভাব্য combination-ও বর্ণনা করে, আর বেশিরভাগই অর্থহীন। loading: true এবং data আছে? Loading শেষ কিন্তু error এবং data দুটোই আছে? data বা error কোনোটাই ছাড়া loading: false-এর মানে কী — এটা কি ব্যর্থ হয়েছে, নাকি শুরুই হয়নি? type খুশি মনে এসব সবকিছুর অনুমতি দেয়, তাই আপনার component-এর কোথাও গিয়ে এমন state-এর জন্য defensive check লিখতে হয় যা প্রথম থেকেই থাকার কথা ছিল না।
সমস্যা হলো এই field-গুলো স্বাধীন নয়। একটা request একবারে ঠিক একটা অবস্থায় থাকে। তাই সেটা একটা discriminated union দিয়ে বলুন:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; message: string };এখন অসম্ভব state-গুলো সত্যিই নেই। loading অবস্থায় data থাকার কোনো উপায় নেই, কারণ loading variant-এ data field-ই নেই। আর ফলটা দেখা যায় যখন আবার পড়েন — status-এর ভিত্তিতে TypeScript type-কে সংকুচিত করে:
function render(state: RequestState) {
switch (state.status) {
case "loading":
return "Loading…";
case "error":
return state.message; // data isn't even in scope here
case "success":
return state.data.name; // and here, data is guaranteed to exist
}
}success branch-এর ভেতরে, state.data User | undefined নয় — এটা User, কোনো optional chaining নেই, কোনো if (data) guard নেই। type system আপনার জন্য guard করে দিয়েছে, কারণ আপনি একে বলেছিলেন এই field-গুলো একসঙ্গে চলে।
স্বাধীন boolean আপনার state গুণ করে দেয় — তিনটা flag মানে আটটা combination, বেশিরভাগই অর্থহীন। একটা discriminated union বলে "এটা এদের মধ্যে ঠিক একটায় আছে," আর অসম্ভব state-গুলো থাকা বন্ধ হয়ে যায়।
satisfies: value চ্যাপ্টা না করে আকৃতি যাচাই
এটা একটা নির্দিষ্ট বিরক্তির সমাধান করে। আপনার একটা config object আছে আর একসঙ্গে দুটো জিনিস চান: এটা প্রত্যাশিত আকৃতির সঙ্গে মেলে কিনা যাচাই করা, এবং value-গুলোর নিখুঁত literal type ধরে রাখা। দীর্ঘদিন আপনি এর একটাই পেতেন।
// Annotate with a type & you lose the specifics.
const routes: Record<string, string> = {
home: "/",
profile: "/profile",
};
routes.home; // type is string — the fact that it's "/" is gone
routes.dashbord; // typo — but no error, any string key is allowedannotation আকৃতি যাচাই করে কিন্তু detail ফেলে দেয়। satisfies আকৃতি যাচাই করে আর detail রেখে দেয়:
const routes = {
home: "/",
profile: "/profile",
} satisfies Record<string, string>;
routes.home; // type is "/", the literal
routes.dashbord; // error: property doesn't exist — typo caughtআপনি Record<string, string>-এর বিরুদ্ধে validate হন — প্রতিটা value একটা string হতে হবে — কিন্তু inferred type সংকীর্ণই থাকে, তাই routes.home হয় "/", আর ভুল বানানের key নীরবে অনুমোদিত string না হয়ে error হয়। মোদ্দা নিয়ম: variable-কে T-তে প্রশস্ত করতে চাইলে একটা type annotation (: T) ব্যবহার করুন, আর T-এর বিরুদ্ধে যাচাই করে detail রাখতে চাইলে satisfies T।
ডেটা দুইবার টাইপ করা বন্ধ করুন — derive করুন
যে অভ্যাস চুপচাপ আপনার maintenance দ্বিগুণ করে দেয় তা হলো একটা value লেখা আর তারপর একই value-এর জন্য হাতে আলাদা একটা type লেখা। এখন এরা আলাদা হয়ে যেতে পারে, আর একদিন যাবেই।
const ROLES = ["admin", "editor", "viewer"] as const;
// Don't hand-write this next to the array — derive it.
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"as const array-টাকে literal-এর একটা readonly tuple-এ জমিয়ে দেয়, আর (typeof ROLES)[number] সরাসরি তার element-গুলোর union পড়ে নেয়। array-এ একটা role যোগ করুন আর type নিজেই হালনাগাদ হয়ে যায় — মনে রাখার মতো দ্বিতীয় কোনো তালিকা নেই। একই ধারণা আপনার পুরো codebase জুড়ে কাজ করে: চাবির জন্য keyof typeof someObject, একটা বড় type থেকে একটা field-এর type বের করতে Config["timeout"]-এর মতো indexed access। মূলনীতি হলো একটাই source of truth থাকা উচিত, আর type-গুলো তা থেকে হিসাব করা উচিত, পাশাপাশি বজায় রাখা নয়।
একটা ভালো পরীক্ষা
একটা value বদলালে যদি মেলাতে দ্বিতীয় জায়গায় একটা type-ও বদলাতে হয়, তাহলে
আপনি একই তথ্য দুইবার বজায় রাখছেন। এটা সাধারণত ইঙ্গিত দেয় যে হাতে না লিখে
typeof / keyof দিয়ে value থেকে type derive করা উচিত।
এসবের নিচে যে ভুলটা লুকিয়ে
উপরের খারাপ ভার্সনগুলোর সাধারণ সুতোটা হলো TypeScript-কে এমন documentation হিসেবে দেখা যা ডেটা ঠিক করার পরে লেখেন। যেভাবে খুশি ডেটার আকৃতি দেন, তারপর যা বানিয়েছেন তা বর্ণনা করে এমন একটা type জুড়ে দেন — তার সব অবৈধ combination-সহ।
পরিবর্তনটা হলো type-কে আগে আসতে দেওয়া আর সীমাবদ্ধ হতে দেওয়া। একটা ভালো type শুধু আপনার ডেটা কী হতে পারে তা বর্ণনা করে না; এটা বাদ দেয় এটা কখনো কী হওয়া উচিত না। state-কে "optional field-এর একটা ব্যাগ"-এর বদলে "এদের মধ্যে ঠিক একটা" হিসেবে model করলে, satisfies-কে আপনার literal ধারালো রাখতে দিলে, আর value থেকে type কপি না করে derive করলে, পুরো এক শ্রেণির বাগ testing-এর বিষয় থেকে compiler-কে লিখতেই না দেওয়া বিষয়ে পরিণত হয়।
এটাই আসল লাভ যা পাওয়া যায়। autocomplete না — যদিও সেটাও পাবেন। এটা হলো ভুল program compile হওয়াই বন্ধ হয়ে যায়। আপনার type-কে খারাপ state নিষিদ্ধ করার দিকে লক্ষ্য করুন, আর TypeScript তার জায়গা অর্জন করা শুরু করে।