সরাসরি পর্বে যান
পর্ব ০৬ · ৯৩-এর মধ্যে · মডিউল ১

TypeScript ফুল-স্ট্যাকের জন্য — type, interface, generic আর ভাগ করা type

TypeScript for Full-Stack Work — Types, Interfaces, Generics and Shared Types

Taught in Bangla, this lesson teaches TypeScript for both sides of an app: inference, unions, literals and narrowing, discriminated unions for honest API results, interface versus type and optional versus nullable, generics such as fetchJson<T>, utility types, React props and events in brief, why types vanish at runtime so outside data must be validated with Zod, shared types and tsconfig in Bazarghar, and why erasing red lines with any is not fixing bugs.

এই পর্বে যা শিখবেন

  • Frontend আর backend-এ type কেন একসাথে লাভ দেয়, আর কোথায় থেমে যায়
  • Union, narrowing আর discriminated union দিয়ে API-র ফল নিরাপদে সামলানো
  • Generic দিয়ে typed API response, আর Partial, Pick, Omit, Record-এর বাস্তব কাজ
  • unknown বনাম any, আর Zod দিয়ে একই schema থেকে type ও চলার সময়ের যাচাই
  • “বাজারঘর”-এ ভাগ করা type, tsconfig-এর কড়া option আর TypeScript 7-এর নতুন compiler

১ · এক মাপ, দুই পাশ — type কেন দুই দিকেই লাভ দেয়

একজন দর্জির দোকান ভাবুন। সামনের কাউন্টারে কর্মচারী মাপ লেখেন, পেছনে কারিগর কাপড় কাটেন। কর্মচারী যদি ইঞ্চিতে লেখেন আর কারিগর সেন্টিমিটার ভাবেন, জামা নষ্ট হবে। ভুলটি ধরা পড়বে ক্রেতা জামা পরার পরে। সমাধান সহজ: দুজনে একই ছাপানো মাপের খাতা ব্যবহার করবেন।

ওয়েব অ্যাপেও একই ঘটনা। Browser-এর কোড হলো কাউন্টার, server-এর কোড হলো কারিগর, আর তাদের মাঝে যায় JSON. দুই পাশে যদি দাম নিয়ে দুই রকম ধারণা থাকে, ভুলটি ধরা পড়ে ব্যবহারকারীর সামনে। TypeScript-এ লেখা একটি ভাগ করা type হলো সেই ছাপানো মাপের খাতা।

একদম প্রথম ধাপগুলো (tsc কী, মৌলিক type, function-এর type) এখানে আবার শেখানো হবে না। সেগুলো আছে JavaScript কোর্সের “TypeScript-এর শুরু” পর্বে. এই পর্বে শুধু সেটুকুই আসবে, যা ফুল-স্ট্যাক কাজে প্রতিদিন লাগে।

এক লাইনে মনে করিয়ে দিই: TypeScript কোড চালানোর আগে একটি type checker সব type মিলিয়ে দেখে, তারপর type মুছে সাধারণ JavaScript বানায়। এবার একটি বাস্তব পরীক্ষা দেখা যাক। কোর্সের চলমান উদাহরণ “বাজারঘর” একটি কাল্পনিক অনলাইন দোকান। তার ছোট একটি project-এ shared/product.ts ফাইলে Product type আছে। সেটি ব্যবহার করে একটি server ফাইল আর একটি React ফাইল। এখন price ঘরের নাম বদলে priceTaka করা হলো।

সত্যিকারের run · লেখার সময়ে TypeScript 7.0.2 · ~/bazarghar

$ npx tsc
client/ProductCard.tsx(4,38): error TS2339: Property 'price' does not exist on type 'Product'.
server/products.ts(4,45): error TS2339: Property 'price' does not exist on type 'Product'.

একটি বদল, দুটি লাল দাগ, দুই আলাদা জগতে। Compiler কোনো request পাঠায়নি, কোনো server চালায়নি। শুধু কোড পড়ে বলেছে, frontend-এর ৪ নম্বর লাইন আর backend-এর ৪ নম্বর লাইন এখন পুরোনো নাম ধরে আছে। Type ছাড়া এই ভুল দেখা যেত পরে, হয়তো পণ্যের কার্ডে “undefined টাকা” হিসেবে।

ভাগ করা type আর চলার সময়ের দেয়াল

Shared types help at build time; validation guards at runtime

ভাগ করা type build-এর সময় কাজ করে, চলার সময় Zod যাচাই পাহারা দেয় shared/ Product · schema client/ React কোড server/ API কোড ওপরে build-এর সময়: tsc নিচে চলার সময়: type নেই Browser শুধু JavaScript JSON Zod যাচাই safeParse মূল কোড নিশ্চিত ডেটা 400 উত্তর ভুলের তালিকা
ছবি ৬.১ — ওপরের অর্ধেকে compiler দুই পাশ মেলায়। দাগের নিচে type নেই, তাই দরজায় যাচাই লাগে।

ছবির দাগটিই এই পর্বের সবচেয়ে বড় কথা। দাগের ওপরে TypeScript অসাধারণ পাহারাদার। দাগের নিচে তার কোনো অস্তিত্ব নেই। পুরো পর্ব জুড়ে আমরা এই দুই ভাগ আলাদা করে দেখব।

TypeScript-এর প্রথম সর্বজনীন version আসে ২০১২ সালে, Microsoft থেকে। নকশার নেতৃত্বে ছিলেন Anders Hejlsberg, যিনি আগে C# তৈরি করেছিলেন। Node.js এসেছিল ২০০৯ সালে। ফলে browser আর server-এ একই ভাষা ব্যবহারের সুযোগ আগেই ছিল। TypeScript সেই এক ভাষার ওপর এক type-ব্যবস্থা এনে দেয়।

২ · Inference, union, literal আর narrowing

সব জায়গায় type লিখতে হয় না। Type inference মানে compiler মান দেখে নিজেই type বুঝে নেয়। অভ্যাসটি হলো: function-এর parameter আর ডেটার আকার আপনি লিখবেন, ভেতরের ছোট ভ্যারিয়েবল compiler-এর হাতে ছেড়ে দেবেন। নিচের প্রতিটি মন্তব্যে compiler-এর বের করা type লেখা আছে।

const city = "dhaka";         // type: "dhaka"   (const বদলাবে না)
let area = "dhaka";           // type: string    (let বদলাতে পারে)
const prices = [500, 140];    // type: number[]

type OrderStatus = "pending" | "paid" | "shipped";
type Id = number | string;

"dhaka" যখন নিজেই একটি type হয়, তাকে বলে literal type. আর | চিহ্ন দিয়ে জোড়া type হলো union type: “এগুলোর যেকোনো একটি”। ফুল-স্ট্যাক কাজে OrderStatus-এর মতো union খুব কাজের। ডেটাবেসে থাকা নির্দিষ্ট কয়েকটি অবস্থার বাইরে কিছু লিখলেই compiler ধরবে, যেমন "payed" বানান ভুল।

Union-এর মান সরাসরি ব্যবহার করা যায় না, কারণ compiler জানে না এখন কোন রূপ। যাচাই করলে সে শাখা ধরে ধরে type সরু করে আনে। একেই বলে narrowing. নিচের ডায়াগ্রামে compiler কোড চালাচ্ছে না, শুধু পড়ছে। প্রতিটি ধাপে v-এর type দেখুন।

1function priceText(v: number | string | null) {2  if (v === null) {3    return "দাম জানা নেই";4  }5  if (typeof v === "string") {6    return v.trim();7  }8  return v.toFixed(2);9}
Compiler-এর চোখে
v
বাদ গেছে
  1. শুরু। লাইন ১-এ v তিনটির যেকোনো একটি হতে পারে। এখনই v.trim() লিখলে error আসবে, কারণ number-এ trim নেই।
  2. প্রথম যাচাই। লাইন ২-এ v === null পরীক্ষা হচ্ছে। শর্তের আগে type এখনো তিনটিই।
  3. সত্য শাখা। লাইন ৩-এ পৌঁছানো মানে শর্ত সত্য, তাই এখানে v শুধু null. number আর string এই শাখায় বাদ।
  4. Return-এর পরের অংশ। লাইন ৩ ফিরে গেছে, তাই লাইন ৫-এ আসতে হলে v null হতে পারে না। Type হলো number │ string. Compiler কোডের গতিপথ অনুসরণ করে।
  5. দ্বিতীয় যাচাই। লাইন ৬ চলে শুধু typeof v === "string" সত্য হলে। তাই v এখানে string, আর trim() বৈধ।
  6. যা বাকি থাকল। লাইন ৮-এ শুধু number বাকি। toFixed কোনো যাচাই ছাড়াই বৈধ, কারণ বাকি দুটি সম্ভাবনা আগেই বাদ পড়েছে।

Narrowing হয় typeof, ===, instanceof, in আর সত্য-মিথ্যা যাচাই দেখে। মনে রাখার বিষয়: এগুলো সাধারণ JavaScript যাচাই, চলার সময়ও সত্যিই চলে। তাই narrowing নিরাপদ, আর পরের অংশে দেখা as নিরাপদ নয়।

৩ · Discriminated union — API-র ফল সৎভাবে লেখা

কুরিয়ারের স্লিপ ভাবুন। “পৌঁছেছে” লেখা স্লিপে থাকে গ্রহীতার সই। “ঠিকানা ভুল” স্লিপে থাকে কোন অংশ ভুল। “গুদামে আটকে আছে” স্লিপে থাকে কারণ। ওপরের ছাপানো ঘরটি দেখেই কর্মচারী বোঝেন, নিচে কোন তথ্য খুঁজতে হবে।

নতুনদের API-র ফল প্রায়ই সব ঘর একসাথে রাখা একটি বস্তার মতো হয়:

type BadResult = {
  loading: boolean;
  data?: Product;
  error?: string;
};

একটু হিসাব করি। তিনটি ঘরের প্রতিটি থাকতে বা না থাকতে পারে, তাই সম্ভাব্য মিশ্রণ ২ × ২ × ২ = ৮টি। অথচ বাস্তবে বৈধ অবস্থা মাত্র ৩টি: লোড হচ্ছে, ডেটা এসেছে, ভুল হয়েছে। বাকি ৫টি অসম্ভব অবস্থা (যেমন একই সাথে ডেটা আর ভুল), কিন্তু type সেগুলো আটকায় না। ফলে প্রতিটি জায়গায় data আছে কি না, নিজে মনে রেখে যাচাই করতে হয়।

Discriminated union এই বস্তাকে আলাদা আলাদা স্লিপে ভাগ করে। প্রতিটি রূপে একই নামের একটি literal ঘর থাকে (এখানে kind). সেই ঘর দেখেই compiler বাকি ঘরগুলো চিনে নেয়। default শাখার never হলো exhaustiveness check: সব রূপ সামলানো হলে সেখানে কিছুই বাকি থাকে না। নিচের ব্লকটি চালিয়ে দেখুন।

type Product = { id: number; name: string; price: number };

type ApiResult<T> =
  | { kind: "ok"; data: T }
  | { kind: "invalid"; fields: string[] }
  | { kind: "error"; status: number; message: string };

function show(r: ApiResult<Product>): string {
  switch (r.kind) {
    case "ok":
      return `${r.data.name}: ${r.data.price} টাকা`;
    case "invalid":
      return `ফর্মে ভুল: ${r.fields.join(", ")}`;
    case "error":
      return `সার্ভারের ভুল ${r.status}: ${r.message}`;
    default: {
      const unreachable: never = r;
      return unreachable;
    }
  }
}

const results: ApiResult<Product>[] = [
  { kind: "ok", data: { id: 7, name: "চিনিগুঁড়া চাল", price: 500 } },
  { kind: "invalid", fields: ["price", "stock"] },
  { kind: "error", status: 503, message: "ডেটাবেস ব্যস্ত" },
];

for (const r of results) console.log(show(r));

আউটপুট:

চিনিগুঁড়া চাল: 500 টাকা
ফর্মে ভুল: price, stock
সার্ভারের ভুল 503: ডেটাবেস ব্যস্ত

তিনটি ফলের জন্য তিন রকম লেখা এসেছে। খেয়াল করুন, case "ok"-এর ভেতরে r.data লেখা যাচ্ছে, অথচ অন্য দুই রূপে data ঘরটিই নেই। T এখানে Product, তাই r.data.price-ও চেনা। অসম্ভব মিশ্রণ এখন আর লেখাই যায় না। React-এ লোডিং দেখানোর জন্য { kind: "loading" } নামে একটি রূপ যোগ করলেই চলে।

এবার ইচ্ছা করেই তিনটি ভুল করা যাক। নিচের ব্লকটি compile হবে না, আর সেটিই উদ্দেশ্য। এই পাতার কোড ব্লকগুলো চলে TypeScript 5.6-এ, --strict সহ। চালিয়ে compiler-এর আসল বার্তা দেখুন।

type Product = { id: number; name: string; price: number };
type ApiResult<T> =
  | { kind: "ok"; data: T }
  | { kind: "invalid"; fields: string[] }
  | { kind: "error"; status: number; message: string };

function showName(r: ApiResult<Product>): string {
  return r.data.name;
}

function label(r: ApiResult<Product>): string {
  switch (r.kind) {
    case "ok":
      return "পাওয়া গেছে";
    case "invalid":
      return "ফর্মে ভুল";
    default: {
      const unreachable: never = r;
      return unreachable;
    }
  }
}

const body: unknown = JSON.parse('{"price":500}');
console.log(body.price + 60);

Compiler তিনটি error দেয়, আর প্রোগ্রাম একটুও চলে না:

prog.ts(8,12): error TS2339: Property 'data' does not exist on type 'ApiResult<Product>'.
  Property 'data' does not exist on type '{ kind: "invalid"; fields: string[]; }'.
prog.ts(18,13): error TS2322: Type '{ kind: "error"; status: number; message: string; }' is not assignable to type 'never'.
prog.ts(25,13): error TS18046: 'body' is of type 'unknown'.

প্রথমটি বলছে, kind যাচাই না করে r.data পড়া যাবে না। সমাধান: if (r.kind === "ok") return r.data.name; আর বাকি ক্ষেত্রের জন্য আলাদা ফেরত মান। দ্বিতীয়টি সবচেয়ে সুন্দর। "error" রূপটি কোনো case-এ ধরা হয়নি, তাই সেটি never ঘরে এসে পড়েছে। Compiler বার্তাতেই বাদ পড়া রূপের নাম দিয়েছে। সমাধান: case "error": যোগ করা। ভবিষ্যতে কেউ union-এ চতুর্থ রূপ যোগ করলেও এই লাইনই সতর্ক করবে। তৃতীয়টি বলছে, unknown যাচাই ছাড়া ছোঁয়া যায় না। এর কারণ অংশ ৮-এ। একই ফাইলে নিজের কম্পিউটারে TypeScript 7.0.2 চালিয়েও এই তিনটি বার্তাই এসেছে।

৪ · interface বনাম type, আর optional বনাম nullable

interface আর type alias নিয়ে অনলাইনে অনেক তর্ক দেখবেন। বাস্তবে object-এর আকার লিখতে দুটোই প্রায় একই কাজ করে। পার্থক্য কয়েকটি জায়গায়:

বিষয়interfacetype
Object-এর আকারপারেপারে
Union, যেমন ApiResult<T>পারে নাপারে
বাড়ানোextends& দিয়ে জোড়া
একই নামে দুবার লেখামিশে এক হয়ে যায়error
সাধারণ ব্যবহারlibrary-র বাড়ানো যায় এমন আকারunion, utility type থেকে বানানো type

কোনটি সেরা, তা নিয়ে সমাজে একমত নেই। কেউ object-এ সবসময় interface লেখেন, কেউ সবখানে type. দুই পথই ঠিক। আসল কথা, দলে একটি নিয়ম মেনে চলা। এই কোর্সে আমরা type লিখব, কারণ API-র ফল প্রায়ই union হয়।

“নেই”, “undefined” আর “null” — তিন আলাদা কথা

ফুল-স্ট্যাকে এই পার্থক্য খুব জরুরি, কারণ ডেটা JSON হয়ে যাতায়াত করে। Optional property (note?: string) মানে ঘরটি না থাকলেও চলে। আর note: string | null মানে ঘরটি থাকবেই, তবে মান ফাঁকা হতে পারে। JSON-এ এদের আচরণও আলাদা: JSON.stringify({ a: undefined, b: null }) দেয় {"b":null}. অর্থাৎ undefined ঘরটি পথেই হারিয়ে যায়, null পৌঁছায়।

লেখাঅর্থবাজারঘরের উদাহরণ (PATCH-এ)
note?: stringঘর না থাকলেও চলেঘর না পাঠালে নোট আগের মতোই থাকবে
note: string | nullঘর থাকবেই, মান ফাঁকা হতে পারেnull পাঠালে নোট মুছে যাবে
note?: string | nullদুটোই সম্ভবনা পাঠালে অপরিবর্তিত, null হলে মুছে ফেলা

এই সব পার্থক্য কাজ করে strictNullChecks চালু থাকলে। এটি বন্ধ থাকলে null আর undefined সব type-এর ভেতরে লুকিয়ে ঢুকে পড়ে। Array.find কিছু না পেলে undefined দেয়। একই ফাইলে option বন্ধ আর চালু, দুই অবস্থায় চালিয়ে দেখা যাক।

type Product = { id: number; name: string };
const products: Product[] = [{ id: 1, name: "চা পাতা" }];
const found = products.find((p) => p.id === 99);
console.log(found.name);

সত্যিকারের run · TypeScript 7.0.2, Node.js 24.15

$ npx tsc --ignoreConfig --noEmit --strictNullChecks false find.ts
(কোনো error নেই)

$ node find.ts
TypeError: Cannot read properties of undefined (reading 'name')
…

$ npx tsc --ignoreConfig --noEmit find.ts
find.ts(4,13): error TS18048: 'found' is possibly 'undefined'.

Option বন্ধ থাকলে compiler চুপ, আর প্রোগ্রাম ভেঙে পড়ে। চালু থাকলে একই ভুল চালানোর আগেই ধরা পড়ে। শেষ কমান্ডে আলাদা করে strict লিখতে হয়নি, কারণ TypeScript 6.0 থেকে এটি নিজে থেকেই চালু। ঠিক করার উপায় হলো যাচাই: if (found) console.log(found.name);. found!.name লিখলে error মুছবে, কিন্তু ভুল থেকে যাবে।

৫ · Generic — type হারায় না এমন API function

মুদি দোকানের কৌটা ভাবুন। কৌটা একই মাপের, কিন্তু গায়ে লেবেল থাকে “চিনি” বা “ডাল”। লেবেল দেখেই বোঝা যায় ভেতরে কী আছে, না খুলেই। Generic হলো type-এর সেই লেবেল। ApiResult<T> কৌটা একটাই, T হলো লেবেলের ফাঁকা জায়গা।

Generic ছাড়া fetchJson লিখলে ফেরত আসবে any, কারণ await res.json() নিজেই any দেয়। তখন r.data.pirce বানান ভুল করলেও কেউ ধরবে না। Generic থাকলে type ঢোকে এক দিক দিয়ে, বের হয় আরেক দিক দিয়ে। নিচের ডায়াগ্রামে দেখুন T কীভাবে function পার হয়। এখানেও compiler কোড চালায় না।

1async function fetchJson<T>(2  path: string, check: (x: unknown) => T3): Promise<ApiResult<T>> { … }45const r = await fetchJson(path, toProduct);6if (r.kind === "ok") console.log(r.data.price);
Compiler-এর হিসাব
T
  1. ডাকার জায়গা। লাইন ৫-এ fetchJson ডাকা হলো, কিন্তু কোথাও <Product> লেখা নেই। T এখনো অজানা। Compiler দেখল, দ্বিতীয় argument toProduct function-টি Product ফেরত দেয়।
  2. মিলিয়ে নেওয়া। লাইন ২ বলছে, check ফেরত দেয় T. আর toProduct ফেরত দেয় Product. দুটি মেলাতে হলে T অবশ্যই Product. এটিও inference.
  3. ফেরত type ভরা। লাইন ৩-এর প্রতিটি T-এর জায়গায় Product বসল। তাই পুরো ফল হলো Promise<ApiResult<Product>>.
  4. Await. লাইন ৫-এর await Promise-এর মোড়ক খুলে দিল। r এখন ApiResult<Product>.
  5. Narrowing. লাইন ৬-এ kind যাচাইয়ের পরে r.data-র type Product. তাই price চেনা, আর pirce লিখলে error আসবে।
  6. চলার সময়। Type মুছে যাওয়ার পরে T-এর কোনো চিহ্ন নেই। সাধারণ JavaScript হিসেবে চলে আর 500 ছাপে। পুরো হিসাবটি ছিল শুধু build-এর সময়ের।

নিচের ব্লকে network নেই। toyServer একটি ছোট Map, যা server-এর জায়গায় JSON লেখা ফেরত দেয়। খেয়াল করুন, দ্বিতীয় পণ্যে price ভুল করে লেখা হয়ে এসেছে "140". check function-টি হলো পাহারাদার।

type Product = { id: number; name: string; price: number };
type ApiResult<T> =
  | { kind: "ok"; data: T }
  | { kind: "error"; status: number; message: string };
const toyServer = new Map<string, string>([
  ["/api/products/7", '{"id":7,"name":"চিনিগুঁড়া চাল","price":500}'],
  ["/api/products/8", '{"id":8,"name":"মসুর ডাল","price":"140"}'],
]);
async function fetchJson<T>(path: string, check: (x: unknown) => T): Promise<ApiResult<T>> {
  const text = toyServer.get(path);
  if (text === undefined) return { kind: "error", status: 404, message: "পাওয়া যায়নি" };
  const raw: unknown = JSON.parse(text);
  try {
    return { kind: "ok", data: check(raw) };
  } catch (e) {
    return { kind: "error", status: 502, message: e instanceof Error ? e.message : "অজানা ভুল" };
  }
}
function toProduct(x: unknown): Product {
  const o = x as Partial<Record<keyof Product, unknown>>;
  if (typeof o?.id !== "number" || typeof o.name !== "string" || typeof o.price !== "number") {
    throw new Error("Product-এর আকার মেলেনি");
  }
  return { id: o.id, name: o.name, price: o.price };
}

async function main() {
  for (const path of ["/api/products/7", "/api/products/8", "/api/products/9"]) {
    const r = await fetchJson(path, toProduct);
    if (r.kind === "ok") console.log("পেলাম:", r.data.name, r.data.price + 60);
    else console.log("ব্যর্থ:", r.status, r.message);
  }
}

main();

আউটপুট:

পেলাম: চিনিগুঁড়া চাল 560
ব্যর্থ: 502 Product-এর আকার মেলেনি
ব্যর্থ: 404 পাওয়া যায়নি

প্রথম পণ্যে 500 + 60 = 560, হিসাব ঠিক। দ্বিতীয়টি toProduct-এ আটকে গেছে, তাই ভুল ডেটা আর ভেতরে ঢোকেনি। না আটকালে এখানে 140 + 60-এর বদলে "14060" হতো। তৃতীয় path Map-এ নেই, তাই 404. fetchJson একবারও Product শব্দটি লেখেনি, তবু r.data.price সংখ্যা হিসেবে চেনা গেছে। আর toProduct-এর ভেতরের as কোনো কিছু প্রমাণ করে না। সে শুধু ঘরগুলো পড়ার সুযোগ দেয়, আসল প্রমাণ পরের typeof যাচাই। হাতে লেখা এই function-এর কষ্ট কমাতেই পরে Zod আসবে।

৬ · Utility type — এক Product থেকে অনেক আকার

একটি Product-এর চেহারা জায়গা ভেদে বদলায়। নতুন পণ্য বানানোর সময় id থাকে না, কারণ সেটি দেয় ডেটাবেস। PATCH-এ যেকোনো একটি ঘর আসতে পারে। ক্রেতাকে কেনা দাম দেখানো যাবে না। প্রতিটির জন্য আলাদা type হাতে লিখলে একই তথ্য চার জায়গায় থাকবে, আর একদিন মিল হারাবে। Utility type একটি মূল type থেকে বাকিগুলো বানিয়ে দেয়।

Utility typeযা করেযেখানে লাগে
Partial<T>সব ঘর optionalPATCH-এর body
Pick<T, K>শুধু নির্দিষ্ট ঘর রাখেপণ্যের কার্ড
Omit<T, K>নির্দিষ্ট ঘর বাদ দেয়POST-এর body, id ছাড়া
Record<K, V>K চাবি আর V মানের objectশহর ধরে stock
type Product = { id: number; name: string; price: number; stock: number; costPrice: number };

type NewProduct = Omit<Product, "id">;              // POST-এর body: id দেয় database
type ProductPatch = Partial<NewProduct>;            // PATCH-এর body: যেকোনো ঘর
type CardView = Pick<Product, "name" | "price">;    // কার্ডে শুধু এ দুটি
type PublicProduct = Omit<Product, "costPrice">;    // ক্রেতার জন্য
type StockByCity = Record<"dhaka" | "sylhet", number>;

const db: Product = { id: 7, name: "মসুর ডাল", price: 140, stock: 12, costPrice: 110 };

const created: NewProduct = { name: "চা পাতা", price: 90, stock: 5, costPrice: 70 };
const patch: ProductPatch = { price: 150 };
const updated: Product = { ...db, ...patch };
const card: CardView = { name: updated.name, price: updated.price };
const stock: StockByCity = { dhaka: 12, sylhet: 0 };

console.log(created.name, updated.price, card, stock.dhaka + stock.sylhet);

// ফাঁদ: Omit শুধু type থেকে বাদ দেয়, মান থেকে নয়
function leaky(p: Product): PublicProduct {
  return p;
}
function safe(p: Product): PublicProduct {
  const { costPrice, ...rest } = p;
  return rest;
}

console.log(JSON.stringify(leaky(db)));
console.log(JSON.stringify(safe(db)));

আউটপুট:

চা পাতা 150 { name: 'মসুর ডাল', price: 150 } 12
{"id":7,"name":"মসুর ডাল","price":140,"stock":12,"costPrice":110}
{"id":7,"name":"মসুর ডাল","price":140,"stock":12}

প্রথম লাইন দেখায় patch কাজ করেছে: দাম 140 থেকে 150, মোট stock 12 + 0 = 12. দ্বিতীয় লাইনটি সতর্কবার্তা। leaky compile হয়েছে, কারণ বাড়তি ঘরওয়ালা object ছোট type-এ বসতে পারে। অথচ JSON-এ costPrice, মানে কেনা দাম, ক্রেতার কাছে চলে গেছে। Omit শুধু type থেকে ঘর বাদ দেয়, মান থেকে নয়। তৃতীয় লাইনের safe সত্যিই ঘরটি সরিয়ে দিয়েছে। API-র উত্তরে কী যাবে, তা type নয়, চলার সময়ের কোডই ঠিক করে।

৭ · React-এ props আর event — এক ঝলক

React শুরু হবে পর্ব ০৭ থেকে, props আসবে পর্ব ০৯-এ, আর event পর্ব ১০-এ। এখন শুধু দেখে রাখুন TypeScript সেখানে কী রূপে থাকে। একটি component আসলে function, আর props তার argument. তাই props-এর type লেখা হয় সাধারণ object type-এর মতোই।

import { useState } from "react";

type AddToCartProps = {
  productId: number;
  maxQty: number;
  onAdd: (id: number, qty: number) => void;
};

export function AddToCart({ productId, maxQty, onAdd }: AddToCartProps) {
  const [qty, setQty] = useState(1);        // inference: number

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setQty(Number(e.currentTarget.value));  // input-এর value সবসময় string
  }

  return (
    <div>
      <input type="number" max={maxQty} value={qty} onChange={handleChange} />
      <button onClick={() => onAdd(productId, qty)}>কার্টে যোগ করুন</button>
    </div>
  );
}

এখানে তিনটি জিনিস খেয়াল করার মতো। useState(1) নিজেই বুঝে নিয়েছে state একটি number. Event handler আলাদা function হলে তার type হাতে লিখতে হয়, যেমন React.ChangeEvent<HTMLInputElement>. আর inline onClick={() => …}-এ কিছু লিখতে হয় না, compiler জায়গা দেখেই বোঝে। এবার component-টি ভুল prop দিয়ে ব্যবহার করে দেখা যাক: <AddToCart productId="7" maxQty={5} onAdd={() => {}} />.

সত্যিকারের run · TypeScript 7.0.2, @types/react 19.3 · শুধু type যাচাই

$ npx tsc
client/AddToCart.tsx(24,32): error TS2322: Type 'string' is not assignable to type 'number'.

লাইন ২৪-এ ভুল ধরা পড়েছে, চলার আগেই। React-এর ক্ষেত্রে এটি অনেক সময় বাঁচায়, কারণ একটি prop-এর নাম বদলালে যতগুলো জায়গায় component ব্যবহার হয়েছে, সব এক তালিকায় চলে আসে।

৮ · Type মুছে যায় — তাই বাইরের ডেটা যাচাই করতেই হবে

ছবি ৬.১-এর দাগের নিচে এবার নামা যাক। Type erasure মানে build-এর সময় সব type মুছে ফেলা। নিচের ফাইলে server থেকে আসা JSON-এ দাম একটি লেখা। কোড as দিয়ে compiler-কে বলছে, “এটি Product, মেনে নিন।” (JSON.parse নিজে any দেয়, তাই compiler আপত্তি করে না।)

type Product = { name: string; price: number };

function withDelivery(p: Product, delivery: number): number {
  return p.price + delivery;
}

const body = '{"name":"চাল","price":"500"}';
const product = JSON.parse(body) as Product;
console.log(withDelivery(product, 60));

সত্যিকারের run · TypeScript 7.0.2 · cart.js হলো tsc-এর লেখা ফাইল

$ npx tsc --ignoreConfig --target es2023 --module esnext --outDir out cart.ts
(কোনো error নেই)

$ cat out/cart.js
"use strict";
function withDelivery(p, delivery) {
    return p.price + delivery;
}
const body = '{"name":"চাল","price":"500"}';
const product = JSON.parse(body);
console.log(withDelivery(product, 60));

$ node out/cart.js
50060

Compiler খুশি, প্রোগ্রাম চলেছে, আর ৫৬০ টাকার বদলে বিল ৫০০৬০ টাকা। ফলাফলের ঘোষিত type number, অথচ আসল মান একটি string. Type আর বাস্তবের এই ফাঁকেই আসল বাগ জন্মায়। Compile করা ফাইলে Product, number, as, কিছুই নেই। চলার সময় পরীক্ষা করার মতো কিছু আর বাকি থাকে না।

মূল ধারণা

Type হলো আপনার কোডের ভেতরের প্রতিশ্রুতি। Network, form, URL, ডেটাবেস, ফাইল, অন্য কোম্পানির API, এগুলো থেকে আসা ডেটা কোনো প্রতিশ্রুতি দেয় না। এই সীমানায় validation লাগবেই। Client-এর ডেটা server কখনো বিশ্বাস করবে না, কারণ যে কেউ browser ছাড়াই সরাসরি request পাঠাতে পারে। এর নিরাপত্তার দিক আছে Cybersecurity কোর্সের API security পর্বে.

unknown বনাম any

any

any মানে “যাচাই বন্ধ”। a.pric.toFixed(2) লিখলেও compiler কিছু বলে না, ভুল ধরা পড়ে চলার সময়। একটি any অন্য ভ্যারিয়েবলেও ছড়িয়ে পড়ে।

যা ইচ্ছা করা যায়

unknown

unknown মানে “কী আছে জানি না, তাই যাচাই না করে ছোঁব না”। যেকোনো মান রাখা যায়, কিন্তু narrowing বা যাচাইয়ের পরেই ব্যবহার করা যায়। অংশ ৩-এর তৃতীয় error এটিই ছিল।

বাইরের ডেটার জন্য

একটি schema, দুটি জিনিস

অংশ ৫-এর toProduct-এর মতো যাচাই হাতে লেখা যায়, কিন্তু type আর যাচাই আলাদা থাকলে একদিন মিল হারায়। Schema library এই দ্বৈততা সরিয়ে দেয়। Zod-এ আপনি একবার নিয়ম লেখেন, আর তা থেকেই চলার সময়ের যাচাই আর build-এর সময়ের type দুটোই পান। Route-এর সাথে পুরো ব্যবহার আসবে পর্ব ২৭-এ, আর form-এ দুই দিকের ব্যবহার পর্ব ২১-এ। এখানে শুধু মূল ধারণাটি দেখুন।

import * as z from "zod";

export const ProductInput = z.object({
  name: z.string().min(2),
  price: z.number().positive(),
  stock: z.int().min(0),
});

export type ProductInput = z.infer<typeof ProductInput>;
import { ProductInput } from "../shared/product-schema.ts";

function handle(body: unknown) {
  const result = ProductInput.safeParse(body);
  if (!result.success) {
    return { status: 400, issues: result.error.issues.map((i) => `${i.path.join(".")}: ${i.code}`) };
  }
  const product: ProductInput = result.data;
  return { status: 201, total: product.price * 2 };
}

console.log(handle({ name: "চিনিগুঁড়া চাল", price: 500, stock: 12 }));
console.log(handle({ name: "চিনিগুঁড়া চাল", price: "500", stock: -1 }));

সত্যিকারের run · লেখার সময়ে Zod 4.6.7, Node.js 24.15

$ npx tsc
(কোনো error নেই)

$ node server/create-product.ts
{ status: 201, total: 1000 }
{ status: 400, issues: [ 'price: invalid_type', 'stock: too_small' ] }

এখানে Node.js 24.15 নিজেই .ts ফাইল থেকে type মুছে চালিয়েছে। মনে রাখবেন, সে type যাচাই করে না, সেই কাজ আগের tsc কমান্ডের। নিচের ডায়াগ্রামে একই schema-র পুরো যাত্রা দেখুন।

  1. Schemaz.objectনিয়ম একবার
  2. Typez.inferbuild-এর সময়
  3. Buildtype মুছলschema থাকল
  4. ঠিক ডেটাsuccess201
  5. ভুল ডেটাissues400
shared/product-schema.tsএকটিমাত্র উৎস
const ProductInput = z.object({
  name: z.string().min(2),
  price: z.number().positive(),
  stock: z.int().min(0),
});
Editor-এ দেখা typez.infer<typeof ProductInput>
{
  name: string;
  price: number;
  stock: number;
}
// positive() বা min(0) type-এ নেই, ওগুলো চলার সময়ের নিয়ম
Build-এর পরেযা browser বা Node পায়
// type ProductInput = … উধাও
const ProductInput = z.object({ … });  // সাধারণ JavaScript মান, রয়ে গেছে
ইনপুট{ name, price: 500, stock: 12 }
safeParse(...)  →  success: true
{ status: 201, total: 1000 }
ইনপুট{ name, price: "500", stock: -1 }
safeParse(...)  →  success: false
{ status: 400, issues: [
  'price: invalid_type',
  'stock: too_small' ] }
  1. নিয়ম একবার লেখা। ProductInput একটি সাধারণ JavaScript object, type নয়। এতে আছে তিনটি ঘর আর তাদের নিয়ম।
  2. Schema থেকে type. z.infer ওই object-এর type পড়ে TypeScript type বানায়। নিয়মে নতুন ঘর যোগ হলে type নিজেই বদলায়, আলাদা করে মেলাতে হয় না। তবে “ধনাত্মক” নিয়মটি type-এ ধরা পড়ে না, type শুধু বলে number.
  3. Build-এর পরে। Type মুছে গেল, কিন্তু schema একটি মান, তাই সে রয়ে গেল। এই কারণেই সে চলার সময়ও কাজ করে। Type মুছে যায়, মান মোছে না।
  4. ঠিক ডেটা। price: 500, stock: 12 সব নিয়ম মানল। result.data এখন ProductInput type-এর, আর এবার সেই type সত্যি কথাই বলছে। উত্তর { status: 201, total: 1000 }.
  5. ভুল ডেটা। price এলো string হয়ে, stock ঋণাত্মক। Zod দুটি সমস্যাই একসাথে জানাল: invalid_type আর too_small. Handler মূল কোডে ঢোকেনি, ফেরত দিল 400.

যাচাইয়ের দাম — byte-এ হিসাব

Type নিজে এক byte-ও পাঠায় না। কিন্তু schema চলার সময়ের কোড, তাই browser-এ পাঠালে তার দাম আছে। একই নিয়ম নিয়ে esbuild 0.28 দিয়ে ছোট ছোট bundle বানিয়ে মাপা হয়েছে (minified, gzip -9).

কী দিয়েminifiedgzip
শুধু TypeScript type (যাচাই নেই)202 B193 B
হাতে লেখা যাচাই340 B270 B
zod/mini17,481 B6,203 B
import * as z from "zod"91,202 B27,050 B
import { z } from "zod"457,456 B94,662 B

এগুলো একটি ছোট নমুনায় একবারের স্থানীয় মাপ, সাধারণ ফলাফল নয়। Bundler আর version বদলালে সংখ্যাও বদলাবে। তবু দুটি শিক্ষা আছে। প্রথমত, type-only ফাইল আর সাধারণ JavaScript ফাইলের minified আকার হুবহু সমান, 202 B: type সত্যিই শূন্য খরচের। দ্বিতীয়ত, import লেখার ধরনেও আকার অনেক বদলাতে পারে। ধরে নিই ব্যবহারকারীর গতি ১ Mbps: 27,050 × 8 ÷ 1,000,000 ≈ 0.22 s, আর 6,203 B-তে ≈ 0.05 s. Server-এ এই খরচ প্রায় গুরুত্বহীন, browser-এ ভাবতে হয়। Bundle মাপার পুরো পদ্ধতি আসবে পর্ব ৭৯-এ।

Browser-এর যাচাই শুধু ব্যবহারকারীর সুবিধার জন্য। আসল যাচাই server-এ হতেই হবে। আর ভাগ করা schema মানে সেই কোড browser-এও যায়, তাই shared/-এ কোনো secret বা ব্যবসার গোপন নিয়ম রাখা যাবে না।

৯ · বাজারঘরে ভাগ করা type, tsconfig আর TypeScript 7

এতক্ষণের ছোট project-এর গঠন ছিল এমন। client আর server কেউ কারও ফাইল import করে না, দুজনেই import করে shared থেকে।

bazarghar/
├── shared/    Product type, ProductInput schema
├── server/    API কোড
├── client/    React কোড
├── package.json
└── tsconfig.json

শুধু type আনতে import type { Product } from "…" লিখুন। এতে নিশ্চিত হয় যে build-এর পরে ওই import পুরোপুরি মুছে যাবে, কোনো server কোড browser-এ যাবে না। বড় project-এ shared সাধারণত npm workspace-এর আলাদা package হয়। আর দুটি সীমা মনে রাখুন। প্রথমত, ভাগ করা type মেলে শুধু এক repository-র ভেতরে, মোবাইল অ্যাপ বা অন্য দলের জন্য চুক্তি লাগে (পর্ব ৩৫-এ OpenAPI, পর্ব ৩৭-এ tRPC). দ্বিতীয়ত, ব্যবহারকারীর browser-এ পুরোনো কোড খোলা থাকতে পারে, আর server নতুন। Type সেই ফাঁক দেখে না।

tsconfig-এর যে option-গুলো কাজে লাগে

tsconfig.json হলো যাচাই কতটা কড়া হবে তার নিয়ম। ওপরের সব run এই ফাইল দিয়েই হয়েছে। Project তৈরির সাধারণ পরিবেশ আছে পর্ব ০৫-এ।

{
  "compilerOptions": {
    "target": "es2023",
    "module": "nodenext",
    "lib": ["es2023", "dom"],
    "jsx": "react-jsx",
    "types": ["node", "react"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "allowImportingTsExtensions": true,
    "noEmit": true
  },
  "include": ["shared", "server", "client"]
}
Optionযা ধরেসত্যিকারের বার্তা
strictকয়েকটি কড়া option একসাথে: strictNullChecks, noImplicitAny, catch-এ unknown ইত্যাদিTS18048: possibly undefined
noUncheckedIndexedAccessprices[5] হয়তো নেই, তাই type-এ undefined যোগ হয়। strict-এর ভেতরে নেইTS18048: 'first' is possibly 'undefined'
exactOptionalPropertyTypesnote?: string-এ undefined বসানো আটকায়। strict-এর ভেতরে নেইTS2375
noEmittsc শুধু যাচাই করবে, ফাইল লিখবে না। JavaScript বানায় Vite বা অন্য টুল—

TypeScript 6.0 থেকে strict নিজে থেকেই চালু, আর types-এর ডিফল্ট ফাঁকা তালিকা। তাই @types প্যাকেজ এখন নিজে থেকে যুক্ত হয় না, এখানে types তালিকায় লিখতে হয়েছে। তবু "strict": true লিখে রাখা ভালো, কারণ পুরোনো version বা অন্য টুলও একই ফাইল পড়তে পারে।

TypeScript 7 — নতুন compiler

লেখার সময়ে npm-এ typescript-এর latest version 7.0.2, আর এই পর্বের স্থানীয় run সেটিতেই হয়েছে। Microsoft-এর ঘোষণা অনুযায়ী ২০২৬ সালের জুলাইয়ে 7.0 এসেছে, যা compiler-কে Go ভাষায় নতুন করে লেখা (native port). প্যাকেজের নাম আর tsc কমান্ড একই আছে। ঘোষণায় বড় project-এ সাধারণত ৮ থেকে ১২ গুণ দ্রুত full build-এর দাবি করা হয়েছে। এটি Microsoft-এর নিজের মাপ।

সত্যিকারের run · নিজে বানানো ৪,০০০ ফাইলের নমুনা project · tsc -p . তিনবার করে

Versionrun ১run ২run ৩গড়
TypeScript 6.0.32766 ms2609 ms2877 ms≈ 2751 ms
TypeScript 7.0.2535 ms519 ms594 ms≈ 549 ms

হিসাব: ৮২৫২ ÷ ১৬৪৮ ≈ ৫ গুণ। এটি একটি কম্পিউটারে একটি কৃত্রিম project-এর মাপ, সাধারণ ফলাফল নয়। ছোট project-এ পার্থক্য কম চোখে পড়বে। আরও মনে রাখুন, 7.0 কিছু পুরোনো option সরিয়ে দিয়েছে (যেমন target: es5, moduleResolution: node10, baseUrl), আর ঘোষণা অনুযায়ী এটি এখনো আগের মতো programmatic API দেয় না। তাই কিছু টুল কিছুদিন 6.0-র ওপর নির্ভর করতে পারে। আপগ্রেডের আগে নিজের টুলের docs দেখে নিন। এই পাতার চালানো যায় এমন ব্লকগুলো এখনো 5.6-এ চলে, তবে এখানে দেখানো ভাষার বিষয়গুলো দুই version-এই একই ফল দিয়েছে।

আরও গভীরে: TypeScript-এর লিখিত Design Goals-এ “fully erasable, structural type system” লক্ষ্য হিসেবে আছে, আর runtime type information যোগ করা স্পষ্টভাবে লক্ষ্যের বাইরে। Type-কে তাই সেট হিসেবে ভাবা যায়: union হলো সেটের যোগ, narrowing হলো সেট ছোট করা, আর never হলো ফাঁকা সেট। এজন্যই সব case সামলালে শেষে never থাকে।

পরীক্ষা ও ইন্টারভিউতে:

  • any আর unknown-এর পার্থক্য? any যাচাই বন্ধ করে, unknown যাচাই ছাড়া ব্যবহার করতে দেয় না।
  • interface আর type-এর মূল পার্থক্য? Union লেখা যায় শুধু type-এ, আর declaration merging হয় শুধু interface-এ।
  • TypeScript থাকলে validation কেন লাগে? Type চলার সময় মুছে যায়, তাই বাইরের ডেটা কেউ পরীক্ষা করে না।
  • Discriminated union কী? প্রতিটি রূপে একটি সাধারণ literal ঘর থাকে, যা দেখে compiler রূপ চিনে নেয়।
  • Omit কি ঘর মুছে দেয়? না, শুধু type থেকে বাদ দেয়, চলার সময়ের মানে ঘরটি থেকে যায়।

১০ · AI-এর যুগে — লাল দাগ মোছা আর ভুল মোছা এক নয়

AI TypeScript খুব দ্রুত লেখে, আর তার কোড প্রায়ই compile-ও হয়। সমস্যা হলো, “compile হয়েছে” মানে সব প্রমাণিত নয়। Error সরাতে AI-এর সবচেয়ে সহজ পথ হলো compiler-কে চুপ করানো। আপনি যদি এই পর্বের দাগটি মনে রাখেন, তার কৌশলগুলো সহজেই ধরতে পারবেন।

AI যা প্রায়ই করেকেন বিপদকীভাবে যাচাই করবেন
any, as বা ! দিয়ে error চাপা দেয়ভুল চলার সময় ফিরে আসেdiff-এ এই তিনটি খুঁজুন, প্রতিটির কারণ জিজ্ঞেস করুন
await res.json() as Product লেখেবাইরের ডেটা যাচাই হয় নাভুল আকারের ডেটা দিয়ে চালিয়ে দেখুন
Zod-এর পুরোনো বা বানানো API দেয়version বদলে API বদলায়লেখার সময়ের docs আর npm view দেখুন, চালান
tsconfig-এ strict: false বা পুরোনো option দেয়পুরো পাহারা দুর্বল হয়, বা TypeScript 7-এ error আসেConfig-এর diff আলাদা করে পড়ুন
// @ts-ignore বসায়পরের লাইনের সব error লুকায়লাইন ধরে খুঁজুন, প্রকৃত কারণ ঠিক করুন
Password বা কেনা দামসহ পুরো object পাঠায়Type ঠিক, ডেটা ফাঁসআসল JSON উত্তরটি খুলে পড়ুন
অপ্রয়োজনীয় package যোগ করেBundle বড় হয়, supply chain ঝুঁকি বাড়েpackage আছে কি না আর দরকার কি না দেখুন

AI-কে TypeScript error ঠিক করতে দিলে আগেই তার পথ বেঁধে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন। শেষ শর্ত দুটিই আসল।

আমার project-এ TypeScript 7.0, strict: true, আর Zod 4 ব্যবহার করছি। নিচের error ঠিক করতে সাহায্য করো: [এখানে tsc-এর পুরো error আর সংশ্লিষ্ট কোড পেস্ট করুন, কোনো secret বা আসল key নয়] ১. Error-টি কেন হচ্ছে, সহজ বাংলায় বলো। ২. সমাধানে any, as, !, @ts-ignore ব্যবহার করবে না। সত্যিই দরকার মনে হলে কারণ লিখবে, আর আগে বিকল্প দেখাবে। ৩. বাইরের ডেটা (network, form) হলে unknown ধরে Zod schema দিয়ে যাচাই দেখাবে। ৪. শুধু যে লাইন বদলাবে সেগুলো দেখাও, আর কোনো নতুন package যোগ করবে না।

AI-এর TypeScript হাতে পেলে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Client আর server একই type ভাগ করলে কি API-র চুক্তি নিয়ে আর ভাবতে হয় না?

না। ভাগ করা type একটি নির্দিষ্ট মুহূর্তের মিল প্রমাণ করে: যে কোড একসাথে build হলো, তারা একে অপরের সাথে মানানসই। কিন্তু production-এ কোড একসাথে চলে না। একজন ব্যবহারকারী সকালে বাজারঘর খুলে ট্যাবটি খোলা রাখলেন। দুপুরে আপনি নতুন server deploy করলেন, যেখানে price হয়ে গেছে priceTaka. তাঁর browser-এ এখনো সকালের JavaScript চলছে, আর compiler সেখানে পৌঁছাতে পারে না।

এজন্য বাস্তবে নাম বদলানো হয় ধাপে ধাপে, যাকে অনেকে expand-contract বলেন। আগে নতুন ঘর যোগ করা হয়, পুরোনোটিও থাকে। সব client সরে গেলে তবেই পুরোনো ঘর সরানো হয়। ডেটাবেস migration-এও একই চিন্তা লাগে (পর্ব ৪০)।

দ্বিতীয় সীমা হলো ভাষা। ভাগ করা .ts ফাইল শুধু TypeScript কোড পড়তে পারে। Android অ্যাপ, অন্য দলের Python সার্ভিস বা বাইরের ক্রেতা সেটি ব্যবহার করতে পারবে না। তাদের জন্য লাগে ভাষা-নিরপেক্ষ চুক্তি: OpenAPI বা GraphQL schema, যা থেকে নানা ভাষার কোড বানানো যায়। সব পাশই যদি TypeScript হয়, tRPC-এর মতো পথে সরাসরি function-এর type ভাগ করা যায়। পর্ব ৩৫ আর ৩৭-এ এই পথগুলোর তুলনা আছে।

কোনটি সেরা, তার একক উত্তর নেই। ছোট দলে এক repository-তে ভাগ করা type দ্রুততম। বাইরের ব্যবহারকারী থাকলে লিখিত চুক্তি আর versioning ছাড়া চলে না, কারণ তাদের আপনি একসাথে বদলাতে পারবেন না।

মূল উপলব্ধি: ভাগ করা type build-এর সময়ের নিরাপত্তা দেয়। Deploy-এর সময়ের নিরাপত্তা আসে সামঞ্জস্য বজায় রাখার অভ্যাস থেকে।

প্র ০২TypeScript ইচ্ছা করেই “unsound”. Compile হওয়া কোডও ভুল type নিয়ে চলতে পারে। এটি কি নকশার ত্রুটি, না সিদ্ধান্ত?

এটি লিখিত সিদ্ধান্ত। TypeScript-এর Design Goals নথির non-goals তালিকায় আছে “sound বা provably correct type system” প্রয়োগ করা। অর্থাৎ দল জেনেবুঝেই সম্পূর্ণ প্রমাণের লক্ষ্য রাখেনি। তারা সঠিকতা আর কাজের সুবিধার মাঝখানে একটি জায়গা বেছে নিয়েছে।

Sound মানে হলো: compiler যদি বলে মানটি number, চলার সময় সেটি সবসময় number হবেই। TypeScript এই গ্যারান্টি দেয় না। সহজ উদাহরণ arrays. PaidOrder[]-কে Order[] ভ্যারিয়েবলে রাখা যায়। এরপর সেই ভ্যারিয়েবলে একটি pending order push করলে compile হয়, অথচ মূল তালিকায় এখন ভুল জিনিস ঢুকে গেছে। any, as আর JSON.parse তো আছেই।

কেন এই ছাড়? কারণ TypeScript-কে বিদ্যমান JavaScript জগতের ওপর বসতে হয়েছে। লক্ষ লক্ষ library এমনভাবে লেখা, যার সব আচরণ প্রমাণ করা কঠিন। পুরো sound ব্যবস্থা হলে অনেক সাধারণ JavaScript pattern error দিত, আর মানুষ হয়তো ভাষাটিই ব্যবহার করত না। আরেকটি non-goal হলো runtime type information যোগ না করা। প্রতিটি assignment-এ চলার সময় পরীক্ষা বসালে কোড ধীর হতো, আর “no runtime overhead” লক্ষ্যও ভাঙত।

অন্যদিকে Flow কিছু ক্ষেত্রে বেশি কড়া পথ নিয়েছিল। Rust বা Haskell-এর মতো ভাষা আরও শক্ত গ্যারান্টি দেয়, তবে তাদের দাম অন্যভাবে দিতে হয়।

মূল উপলব্ধি: TypeScript-কে প্রমাণযন্ত্র না ভেবে খুব ভালো সহকারী ভাবুন। সে বেশিরভাগ ভুল ধরে, সব নয়। তাই test আর সীমানার যাচাই বাদ দেওয়া যায় না।

প্র ০৩Tony Hoare কেন null-কে নিজের “billion-dollar mistake” বলেছিলেন? strictNullChecks এই ইতিহাসে কোথায় বসে?

Tony Hoare ১৯৬৫ সালে ALGOL W ভাষায় null reference যোগ করেন, কারণ সেটি বানানো সহজ ছিল। ২০০৯ সালে QCon সম্মেলনে তিনি একে নিজের “billion-dollar mistake” বলেন। তাঁর বক্তব্য ছিল, এই একটি সিদ্ধান্ত থেকে অগণিত ভুল, দুর্বলতা আর crash এসেছে। সংখ্যাটি হিসাব করা নয়, তাঁর আন্দাজ।

মূল সমস্যাটি হলো type-এর মিথ্যা। Java-র মতো ভাষায় String name লিখলে সেটি আসলে “String অথবা null”, কিন্তু type সেকথা বলে না। প্রতিটি ব্যবহার তাই একটি লুকানো ঝুঁকি। JavaScript-এর অবস্থা আরও জটিল, কারণ এখানে null আর undefined দুটোই আছে। “Cannot read properties of undefined” বার্তাটি ওয়েব ডেভেলপারদের সবচেয়ে পরিচিত বার্তাগুলোর একটি।

আধুনিক ভাষাগুলো একই সমাধানের দিকে গেছে: ফাঁকা হওয়ার সম্ভাবনা type-এ লেখা থাকবে। Rust-এ Option, Kotlin-এ String?, Swift-এ optional. TypeScript 2.0 (২০১৬) এনেছে strictNullChecks, যেখানে string মানে শুধু string, আর ফাঁকা হতে পারলে লিখতে হয় string | null. পুরোনো কোড ভাঙার ভয়ে এটি ঐচ্ছিক রাখা হয়েছিল। ২০১৭ সালের 2.3-এ strict flag আসে, আর TypeScript 6.0-তে সেটি ডিফল্ট হয়।

Control flow analysis এখানে গুরুত্বপূর্ণ। if (found) লিখলেই ভেতরে compiler মেনে নেয়, তাই অতিরিক্ত wrapper লাগে না। এটি অংশ ২-এর narrowing-ই।

মূল উপলব্ধি: ভালো নকশা ফাঁকা অবস্থাকে লুকায় না, type-এ প্রকাশ করে। তখন “ভুলে গেছি” কথাটি compiler-এর error হয়ে যায়।

প্র ০৪TypeScript compiler নিজের ভাষা ছেড়ে Go-তে গেল কেন? আর এর দাম কী?

TypeScript compiler দীর্ঘদিন TypeScript-এই লেখা ছিল, আর Node.js-এ JavaScript হিসেবে চলত। নিজের ভাষায় নিজের compiler লেখাকে বলে self-hosting. কিন্তু বড় project-এ যাচাইয়ে সময় লাগত, editor খুলতে দেরি হতো, আর মেমরিও বেশি লাগত। Compiler-এর মতো CPU-নির্ভর দীর্ঘ কাজ JavaScript-এর একক থ্রেডের জন্য আদর্শ নয়।

২০২৫ সালের মার্চে Anders Hejlsberg native port-এর ঘোষণা দেন, ভেতরের নাম ছিল Project Corsa. এটি নতুন করে নকশা নয়, বরং বিদ্যমান কোডের অনুবাদ। “কেন Go” প্রশ্নে দলের প্রকাশ্য আলোচনায় মূল যুক্তি ছিল সামঞ্জস্য। Go-র কোড বিদ্যমান কোডের গঠনের কাছাকাছি, তাই দুই codebase-এর মধ্যে পরিবর্তন আনা সহজ। Object-এর স্মৃতি-বিন্যাস নিয়ন্ত্রণ করা যায়, অথচ সব কোডে হাতে মেমরি সামলাতে হয় না। Compile শেষে process বন্ধ হয়ে যায়, তাই garbage collection-এর খরচ এই কাজে কম বলে তাঁরা মনে করেন।

২০২৬ সালের জুলাইয়ে এলো 7.0. ঘোষণায় full build-এ সাধারণত ৮ থেকে ১২ গুণ গতির কথা বলা হয়েছে। আমাদের ছোট কৃত্রিম নমুনায় একবারের মাপে পার্থক্য ছিল প্রায় ৫ গুণ। মাপের ভিন্নতাই শেখায় যে ঘোষিত সংখ্যা নিজের কাজে যাচাই করে নিতে হয়।

দাম আছে। JavaScript-এ লেখা অনেক টুল compiler-কে সরাসরি library হিসেবে ব্যবহার করত। ঘোষণা অনুযায়ী 7.0-তে সেই API নেই, নতুন API পরে আসার কথা। তাই কিছু টুল কিছুদিন 6.0-র পাশাপাশি চলতে পারে। Compiler-এ অবদান রাখতে এখন Go-ও জানতে হয়।

মূল উপলব্ধি: বড় প্রকৌশলী সিদ্ধান্ত প্রায়ই “সেরা ভাষা” খোঁজা নয়, বরং বিদ্যমান কোড, দল আর লক্ষ্যের সাথে সবচেয়ে মানানসই পথ খোঁজা।

প্র ০৫“Parse, don't validate” কথাটির মানে কী? আর exception ছুড়ে দেওয়া না ApiResult ফেরত দেওয়া, কোনটি ভালো?

২০১৯ সালে Alexis King “Parse, don't validate” নামে একটি প্রবন্ধ লেখেন, যা type-নিরাপত্তার আলোচনায় খুব পরিচিত হয়। মূল ভাবনা: শুধু হ্যাঁ বা না বলা যাচাই একটি তথ্য ফেলে দেয়। isValid(x) সত্য হলেও পরের কোড জানে না কী প্রমাণ হলো, তাই আবার যাচাই করতে হয়। বরং অনিশ্চিত ডেটা ঢুকিয়ে নিশ্চিত type-এর ডেটা বের করে আনুন। প্রমাণটি তখন type-এর ভেতরে থেকে যায়।

Zod-এর safeParse ঠিক এই আকারের। ইনপুট unknown, আর সফল হলে result.data একটি পরিষ্কার ProductInput. অজানা ঘরগুলোও ডিফল্টে বাদ পড়ে। ফলে সীমানা পার হওয়ার পরে ভেতরের কোডে আর সন্দেহ লাগে না।

দ্বিতীয় প্রশ্নে ভাষাগুলো ভিন্ন পথ নিয়েছে। Rust-এ Result ফেরত দেওয়া হয়, আর Go-তে error একটি সাধারণ ফেরত মান। Java বা JavaScript-এ exception প্রচলিত। TypeScript-এ throw-এর type লেখা যায় না, আর catch-এ আসা মান unknown. তাই কোন function কী ভুল ছুড়তে পারে, compiler জানে না।

বাস্তব নিয়ম হিসেবে অনেকে এভাবে ভাগ করেন: প্রত্যাশিত ব্যর্থতা (ভুল ইনপুট, পণ্য নেই, stock শেষ) union হিসেবে ফেরত দিন, যাতে কলকারী সামলাতে বাধ্য হন। অপ্রত্যাশিত ব্যর্থতা (ডেটাবেস বন্ধ, কোডের বাগ) exception হিসেবে উঠুক, আর একটি কেন্দ্রীয় handler ধরুক (পর্ব ২৮)। সবাই একমত নন, আর Express-এর মতো framework-এর ধরনও এতে প্রভাব ফেলে।

মূল উপলব্ধি: সীমানায় অনিশ্চয়তাকে নিশ্চিত type-এ রূপান্তর করুন, আর যে ব্যর্থতা স্বাভাবিক ঘটনা, তাকে type-এ লিখে রাখুন।

অনুশীলন

নিজে চালিয়ে, হিসাব করে আর ভেবে করুন। তারপর উত্তর মিলিয়ে নিন।

  1. কমান্ড: একটি ফাঁকা ফোল্ডারে npm init -y আর npm i -D typescript চালান। অংশ ৪-এর find.ts ফাইলটি বানিয়ে --strictNullChecks false দিয়ে আর ছাড়া, দুইভাবে tsc চালান। কী পার্থক্য দেখলেন? এরপর কোড ঠিক করুন।

    Option বন্ধ থাকলে কোনো error আসে না। চালু থাকলে আসে TS18048: 'found' is possibly 'undefined'. ঠিক করা কোড: if (found) console.log(found.name); else console.log("পাওয়া যায়নি");. ফোল্ডারে tsconfig.json থাকলে TypeScript 7-এ ফাইলের নাম দিয়ে চালাতে --ignoreConfig লাগে। আপনার version আলাদা হলে আচরণ একটু ভিন্ন হতে পারে, npx tsc --version দেখে নিন।

  2. হিসাব: একটি ফর্মে Zod যোগ করায় bundle বাড়ল 27,050 byte (gzip). zod/mini-তে বাড়ে 6,203 byte. ব্যবহারকারীর গতি ধরে নিন ২ Mbps. দুই ক্ষেত্রে এই অংশ নামাতে কত সময় লাগবে, আর পার্থক্য কত? (সংখ্যাগুলো ধরে নেওয়া, latency বাদ।)

    27,050 × 8 ÷ 2,000,000 = 0.108 s, মানে প্রায় ১০৮ মিলিসেকেন্ড। 6,203 × 8 ÷ 2,000,000 = 0.025 s, মানে প্রায় ২৫ মিলিসেকেন্ড। পার্থক্য প্রায় ৮৩ মিলিসেকেন্ড। ডাউনলোডের পরে JavaScript parse করা আর চালানোর সময়ও যোগ হবে, যা এই হিসাবে নেই। Admin প্যানেলে এটি হয়তো নগণ্য, কিন্তু কম গতির ফোনে প্রথম পাতায় ভাবার মতো।

  3. আউটপুট অনুমান: নিচের কোড compile হবে কি? হলে কী ছাপবে?
    type Item = { name: string; qty: number };
    const raw = JSON.parse('{"name":"ডিম","qty":"3"}') as Item;
    console.log(raw.qty + 1);
    console.log(raw.qty * 2);

    Compile হবে, কারণ JSON.parse দেয় any, আর as compiler-কে Item মানিয়ে দিয়েছে। আউটপুট প্রথমে 31, তারপর 6. আসল মান string "3", তাই + জোড়া লাগায়, আর * সংখ্যায় বদলে গুণ করে। Type বলছে দুটোই number, বাস্তব অন্য কথা। সমাধান হলো schema দিয়ে যাচাই, as নয়।

  4. নকশা ও সমালোচনা: একজন সহকর্মী Express route-এ লিখেছেন: const p = req.body as Product; await db.save(p); res.json(p);. Product-এ costPrice-ও আছে। অন্তত তিনটি সমস্যা বলুন, আর উন্নত নকশা দিন।
    • as কিছু যাচাই করে না। ক্লায়েন্ট যা খুশি পাঠাতে পারে, যেমন string দাম বা ঋণাত্মক stock.
    • বাড়তি ঘর সরাসরি ডেটাবেসে যেতে পারে। id বা costPrice ক্লায়েন্ট ঠিক করে দিতে পারে। একে mass assignment বলে।
    • উত্তরে costPrice ফাঁস হয়। Type নয়, চলার সময়ের মানই পাঠানো হচ্ছে।
    • কে পণ্য বানাতে পারে, যাচাই নেই। Permission আসবে পর্ব ৪৮-এ।

    উন্নত নকশা: ProductInput.safeParse(req.body), ব্যর্থ হলে 400. সফল হলে result.data দিয়ে সেভ, কারণ অজানা ঘর সেখানে নেই। উত্তরে ঘর বেছে একটি PublicProduct বানিয়ে পাঠানো। পুরো পথটি আসবে পর্ব ২৭-এ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমভাগ করা schema থেকে সার্ভারে যাচাই পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. shared/-এ Zod schema লেখা
  2. z.infer দিয়ে type বের করা
  3. tsc client আর server দুই পাশের কোড মিলিয়ে দেখে
  4. Build-এ type মুছে যায়, schema থেকে যায়
  5. Browser JSON পাঠায়
  6. Server safeParse চালিয়ে ডেটা গ্রহণ বা বাতিল করে

TypeScript strict mode-এ const x: unknown = JSON.parse(s); console.log(x.price); লিখলে কী হবে?

  • Compile হবে, চলার সময় মান ছাপবে
  • Compile-এর সময় error আসবে
  • Compile হবে, চলার সময় TypeError আসবে
  • x স্বয়ংক্রিয়ভাবে any হয়ে যাবে

একটি type: type R = { kind: "ok"; data: string } | { kind: "fail"; code: number }. r: R-এর data নিরাপদে পড়ার ঠিক উপায় কোনটি?

  • সরাসরি r.data
  • (r as { data: string }).data
  • if (r.kind === "ok") শর্তের ভেতরে r.data
  • const a: any = r; a.data

User type-এ password ঘর আছে। function f(u: User): Omit<User, "password"> { return u; } compile হলো। JSON.stringify(f(user))-এ কী থাকবে?

  • password ঘরটিও থাকবে
  • password ঘরটি মুছে যাবে
  • Compile error আসবে
  • ফাঁকা {} আসবে

কোড: const o = JSON.parse('{"qty":"3"}') as { qty: number }; console.log(o.qty + 1);. strict mode-এ কী ঘটবে?

  • Compile error
  • 4 ছাপবে
  • চলার সময় TypeError
  • 31 ছাপবে

function first<T>(xs: T[]): T | undefined { return xs[0]; }. first(["চাল", "ডাল"])-এর type কী?

  • any
  • string | undefined
  • string
  • unknown

একটি Zod schema আর তা থেকে বানানো z.infer type-এর মধ্যে build-এর পরে চলার সময় কী থাকে?

  • দুটোই মুছে যায়
  • শুধু type থাকে
  • শুধু schema থাকে
  • দুটোই থাকে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ভাগ করা type frontend আর backend-এর ভুল build-এর সময় একসাথে ধরে।
  2. Discriminated union অসম্ভব অবস্থা বাদ দেয়, আর never প্রমাণ করে কোনো রূপ বাদ পড়েনি।
  3. Generic type-এর তথ্য function পার করে দেয়, আর utility type এক type থেকে বাকিগুলো বানায়।
  4. Type চলার সময় থাকে না। Omit ঘর মোছে না, as যাচাই করে না।
  5. বাইরের ডেটা unknown ধরে schema দিয়ে যাচাই করুন, আর AI-এর any ও as নিজে পরীক্ষা করুন।

পরের পর্বে: মডিউল ১ শেষ। এবার React শুরু। Component-এ UI ভাবা কেন এত বড় পরিবর্তন, আর বাজারঘরের পণ্যের পাতা কীভাবে ছোট ছোট টুকরোয় ভাগ হয়, তা দেখবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান