সরাসরি পর্বে যান
পর্ব ৪৭ · ৫৯-এর মধ্যে · মডিউল ৭

TypeScript-এর শুরু — টাইপ দিয়ে নিরাপদ JavaScript

Introduction to TypeScript — Annotations, Inference, Unions & Narrowing, Generics and Validating Real Data

Taught in Bangla, this lesson introduces TypeScript as checked JavaScript: compile-time errors versus runtime errors, setting up tsc with a strict tsconfig, basic types, inference and any, unknown and never, typing functions and objects with type and interface, unions, narrowing and discriminated unions, generics and utility types, the risks of as and non-null assertions, and validating API data.

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

  • TypeScript কী সমস্যার সমাধান করে, আর কীভাবে কাজ করে — লেখা, যাচাই, টাইপ মোছা, চালানো
  • প্রজেক্টে সেটআপ: tsc, tsconfig.json, strict, Vite আর Node.js-এর সাথে ব্যবহার
  • মৌলিক টাইপ, inference, any বনাম unknown বনাম never, function আর object-এর টাইপ
  • structural typing, union, narrowing, discriminated union, literal type, generics আর utility type
  • বাস্তব প্রজেক্টে: API-র ডেটা নিরাপদে নেওয়া, as-এর বিপদ, enum বনাম union, .d.ts আর পুরোনো JavaScript প্রজেক্ট ধীরে ধীরে TypeScript-এ নেওয়া

১ · সমস্যা: JavaScript টাইপের ভুল ধরে শুধু চলার সময়

পর্ব ০৬-এ দেখেছেন, JavaScript dynamically typed — টাইপ থাকে মানের গায়ে, আর যাচাই হয় কোড চলার মুহূর্তে। ছোট script-এ এটি আরামদায়ক। কিন্তু একটি অনলাইন দোকানের কোড ভাবুন, যেখানে দশজন মানুষ দুই বছর ধরে কাজ করছেন। নিচের কোডে দুটি ভুল আছে। খুঁজে পান কি না দেখুন:

function cartTotal(items, delivery) {
  const sub = items.reduce((sum, it) => sum + it.price * it.qty, 0);
  return sub + delivery;
}

const cart = [{ name: "চা", price: 20, qty: 3 }, { name: "সিঙ্গারা", price: 10, qty: 4 }];
const deliveryInput = "60";              // input.value — সবসময় string
console.log(cartTotal(cart, deliveryInput));

const order = { id: 7, total: 160 };
console.log(order.totl * 2);             // total-এর বানান ভুল

প্রত্যাশিত আউটপুট: প্রথম লাইনে 10060, দ্বিতীয় লাইনে NaN. মোট দাম ১০০ টাকা আর ডেলিভারি ৬০ — হওয়ার কথা ১৬০. কিন্তু deliveryInput একটি string, তাই 100 + "60" জোড়া লেগে হলো "10060" (পর্ব ০৬-এর coercion). দ্বিতীয় ভুলে order.totl নামের কোনো property নেই, তাই মান undefined, আর undefined * 2 হলো NaN. সবচেয়ে বিপজ্জনক বিষয়: কোনো error আসেনি। প্রোগ্রাম আনন্দে চলেছে, আর গ্রাহক দেখেছেন দশ হাজার ষাট টাকার বিল।

এই ভুলগুলো কে ধরবে? JavaScript-এ উত্তর হলো: যিনি কোডটি চালাবেন. ভাগ্য ভালো হলে আপনি নিজে, টেস্ট লেখার সময় (পর্ব ৪৮)। ভাগ্য খারাপ হলে ব্যবহারকারী, রাত দুটোয়। সমস্যাটা মৌলিক: “এখানে সংখ্যা আসার কথা”, “Order-এ total নামে ঘর আছে” — এই জ্ঞান প্রোগ্রামারের মাথায় থাকে, কোডে লেখা থাকে না। তাই কোনো টুল তা যাচাই করতে পারে না।

ঢাকা মেট্রোরেলের কথা ভাবুন। টিকিট যাচাই হয় স্টেশনে ঢোকার গেটেই — ট্রেন চলার পর মাঝপথে নয়। ভুল টিকিট থাকলে গেট খোলে না, আর কোনো যাত্রী ভুল ট্রেনে উঠে পরে বিপদে পড়েন না। JavaScript যেন গেটহীন স্টেশন: যে কেউ উঠে পড়ে, ভুল ধরা পড়ে চলন্ত ট্রেনে। TypeScript সেই গেট বসায় — কোড চালানোর আগে, আপনার নিজের কম্পিউটারে।

যেসব ভাষায় টাইপ কোডে লেখা থাকে আর চালানোর আগেই যাচাই হয় (C, C++, Java), তাদের বলে statically typed. TypeScript JavaScript-কে এই সুবিধা দেয় — কিন্তু JavaScript-কে বদলে না দিয়ে। কীভাবে, সেটিই এই পর্বের মূল গল্প।

২ · TypeScript কী — টাইপ লেখা, আগে যাচাই, তারপর মুছে ফেলা

TypeScript হলো JavaScript-এর একটি superset: সব বৈধ JavaScript কোড (প্রায়) বৈধ TypeScript, আর TypeScript তার ওপর যোগ করে একটি জিনিস — type annotation, অর্থাৎ নামের পরে : টাইপ লিখে বলে দেওয়া কোন মান কোন ধরনের। function cartTotal(items: Item[], delivery: number): number পড়েই বোঝা যায়: দ্বিতীয় argument অবশ্যই number, আর ফলও number.

এর চেয়েও গুরুত্বপূর্ণ হলো TypeScript কী করে না. এটি নতুন runtime নয়; browser বা Node.js কখনো TypeScript চালায় না। TypeScript compiler (tsc) দুটি আলাদা কাজ করে: টাইপ যাচাই, আর টাইপ মুছে সাধারণ JavaScript তৈরি। একে বলে type erasure. নিচের ডায়াগ্রামে ধাপগুলো দেখুন:

  1. আপনি লেখেনcart.tsJavaScript + টাইপ
  2. Checker যাচাই করেtsc / editorচালানো ছাড়াই
  3. error আছে?TS2345…লাইন ও কারণসহ
  4. টাইপ মোছাcart.jsemit বা strip
  5. চলেbrowser / Nodeটাইপের চিহ্নও নেই
cart.tsআপনার লেখা
type Item = { price: number; qty: number };
function total(items: Item[], delivery: number): number {
  return items.reduce((s, it) => s + it.price * it.qty, 0) + delivery;
}
total(cart, "60");
Type checkerপ্রতিটি মান আর টাইপ মেলানো
// delivery-র টাইপ: number
// পাঠানো মান "60"-এর টাইপ: string
// string কি number-এর জায়গায় বসে? — না
Terminal / editor-এর লাল দাগচালানোর আগেই
cart.ts(4,13): error TS2345: Argument of type 'string'
  is not assignable to parameter of type 'number'.
cart.jsটাইপ মুছে যা থাকে
function total(items, delivery) {
  return items.reduce((s, it) => s + it.price * it.qty, 0) + delivery;
}
// type Item … লাইনটি পুরোপুরি উধাও
চলার সময়শুধুই JavaScript
// engine জানেই না কোনোদিন টাইপ ছিল
// বাইরে থেকে ভুল ডেটা এলে কেউ আটকাবে না
// (তাই অংশ ৮-এ যাচাইয়ের কৌশল)
  1. লেখা. আপনি .ts ফাইলে সাধারণ JavaScript লেখেন, শুধু কিছু জায়গায় টাইপ যোগ করেন: parameter-এর পরে : number, ডেটার আকার বোঝাতে type Item = {…}.
  2. যাচাই. Type checker কোড চালায় না, শুধু পড়ে। প্রতিটি মান কোথা থেকে আসছে আর কোথায় যাচ্ছে, টাইপ মেলে কি না দেখে। VS Code এই কাজ আপনি টাইপ করার সাথে সাথেই করে — লাল ঢেউখেলানো দাগ সেখান থেকেই আসে।
  3. error. না মিললে ফাইলের নাম, লাইন-কলাম, একটি কোড (TS2345) আর ইংরেজি ব্যাখ্যা দেয়। ভুলটি ধরা পড়ল কোড একবারও না চালিয়ে — মেট্রোর গেটে।
  4. টাইপ মোছা. এরপর টাইপগুলো মুছে ফেলা হয়। tsc নিজে .js ফাইল লিখতে পারে (emit), অথবা Vite, esbuild বা Node.js নিজেই চালানোর মুহূর্তে টাইপ মুছে নেয় (strip). বাকি কোড প্রায় হুবহু একই থাকে।
  5. চালানো. Browser বা Node.js পায় সাধারণ JavaScript. চলার সময় কোনো টাইপ-যাচাই নেই। সার্ভার যদি price-এর জায়গায় "500" পাঠায়, TypeScript তা জানবে না — এটিই এই পর্বের সবচেয়ে গুরুত্বপূর্ণ সতর্কতা।

নিজে চালিয়ে দেখুন — প্রথমে ঠিক কোড, তারপর ভুল কোড

নিচের কোড ব্লকটি সত্যিকারের TypeScript 5.6 compiler-এ যায়: আগে টাইপ যাচাই, সব ঠিক থাকলে তারপর চলে। একটু সময় লাগে (৫–১০ সেকেন্ড)। Order টাইপের | চিহ্ন আর never-এর কৌশল অংশ ৬-এ বিস্তারিত দেখবেন — এখন শুধু চালিয়ে দেখুন।

type Item = { name: string; price: number; qty: number };
type Order =
  | { status: "pending"; id: number }
  | { status: "paid"; id: number; paidAt: string }
  | { status: "shipped"; id: number; courier: string };
function cartTotal(items: Item[], delivery: number): number {
  const sub = items.reduce((sum, it) => sum + it.price * it.qty, 0);
  return sub + delivery;
}
function deliveryText(o: Order): string {
  switch (o.status) {
    case "pending": return `#${o.id}: পেমেন্টের অপেক্ষায়`;
    case "paid":    return `#${o.id}: পেমেন্ট হয়েছে, তারিখ ${o.paidAt}`;
    case "shipped": return `#${o.id}: ${o.courier} কুরিয়ারে পাঠানো হয়েছে`;
    default: {
      const unreachable: never = o;   // কোনো case বাদ পড়লে এখানে error
      return unreachable;
    }
  }
}
const cart: Item[] = [{ name: "চা", price: 20, qty: 3 }, { name: "সিঙ্গারা", price: 10, qty: 4 }];
console.log("মোট:", cartTotal(cart, 60));
console.log(deliveryText({ status: "paid", id: 7, paidAt: "১০ অক্টোবর" }));
console.log(deliveryText({ status: "shipped", id: 8, courier: "ঢাকা এক্সপ্রেস" }));

আউটপুট তিন লাইন: মোট: 160, #7: পেমেন্ট হয়েছে, তারিখ ১০ অক্টোবর আর #8: ঢাকা এক্সপ্রেস কুরিয়ারে পাঠানো হয়েছে. খেয়াল করুন, case "paid"-এর ভেতরে o.paidAt লেখা যাচ্ছে, অথচ "pending" অবস্থার Order-এ paidAt নেই। Compiler switch দেখে বুঝে নিয়েছে, ওই শাখায় o অবশ্যই paid অবস্থার। ভুল করে case "pending"-এর ভেতরে o.courier লিখলে সাথে সাথে error দিত।

এবার অংশ ১-এর সেই দুটি ভুল TypeScript-এ লিখি। data-expect-error চিহ্নিত এই ব্লক ইচ্ছা করেই ভুল — চালিয়ে দেখুন কী হয়:

type Item = { name: string; price: number; qty: number };
type Order = { id: number; total: number };

function cartTotal(items: Item[], delivery: number): number {
  const sub = items.reduce((sum, it) => sum + it.price * it.qty, 0);
  return sub + delivery;
}

const cart: Item[] = [{ name: "চা", price: 20, qty: 3 }];
const deliveryInput = "60";          // input.value সবসময় string
console.log("শুরু হলো");
console.log(cartTotal(cart, deliveryInput));

const order: Order = { id: 7, total: 160 };
console.log(order.totl * 2);

Compiler দুটি error দেয়, আর প্রোগ্রাম একেবারেই চলে না — এমনকি প্রথম লাইনের শুরু হলো-ও ছাপা হয় না:

prog.ts(12,29): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
prog.ts(15,19): error TS2551: Property 'totl' does not exist on type 'Order'. Did you mean 'total'?

পড়ার নিয়ম: prog.ts হলো ফাইল (এই অনলাইন compiler ফাইলটির নাম দেয় prog.ts), (12,29) মানে লাইন ১২-র ২৯ নম্বর অক্ষর — ঠিক deliveryInput যেখানে শুরু। প্রথম বার্তা: “string টাইপের argument number টাইপের parameter-এ বসানো যায় না।” দ্বিতীয়টি আরও সাহায্যকারী: “Order টাইপে 'totl' নেই। আপনি কি 'total' বোঝাতে চেয়েছেন?” যে দুটি ভুল JavaScript-এ নিঃশব্দে ভুল বিল বানাত, সেগুলো এখানে চালানোর আগেই ধরা পড়ল। ঠিক করতে লিখুন Number(deliveryInput) (তার আগে খালি কি না যাচাই, পর্ব ৩৯) আর order.total.

এই অনলাইন compiler সম্পর্কে একটি কথা। এটি tsc 5.6 চালায় --strict আর --target ES2022 দিয়ে — অর্থাৎ null-সংক্রান্ত সতর্কতাসহ সব strict যাচাই চালু, আর Promise বা Array.prototype.at-এর মতো আধুনিক API-ও চেনে। নিজের প্রজেক্টেও ঠিক এভাবে strict: true আর আধুনিক target দিন (পরের অংশ)। এই পর্বে দেখানো strict mode-এর error বার্তাগুলো একই সেটিং-এ চালিয়ে নেওয়া।

বিনিময়ে কী পান, আর কী দিতে হয়

✅ যা পান

Editor-এ সাথে সাথে error; নির্ভুল autocomplete (order. লিখলেই সব ঘরের তালিকা); নিরাপদ refactoring — একটি ঘরের নাম বদলালে সব ব্যবহারের জায়গা একসাথে বদলায়, বাদ পড়লে error; আর টাইপ নিজেই হালনাগাদ থাকা ডকুমেন্টেশন।

বড় দল, দীর্ঘ প্রজেক্টে লাভ বেশি

⚖️ যা দিতে হয়

টাইপ লিখতে আর বজায় রাখতে সময়; একটি build বা check ধাপ; শেখার খরচ — বিশেষ করে জটিল generic-এর error পড়া; আর মাঝে মাঝে compiler-কে বোঝাতে কষ্ট যে আপনার কোড আসলে ঠিক।

ছোট script-এ খরচ বেশি মনে হতে পারে

Microsoft ২০১২ সালের অক্টোবরে TypeScript-এর প্রথম সর্বজনীন version (0.8) প্রকাশ করে। নকশার নেতৃত্বে ছিলেন Anders Hejlsberg — যিনি আগে Turbo Pascal, Delphi আর C# তৈরি করেছিলেন। শুরু থেকেই এটি open source. ২০১৫ সালে Google-এর Angular দল তাদের নতুন framework TypeScript-এ লেখার ঘোষণা দেয়, আর একই বছর প্রকাশিত VS Code নিজেও TypeScript-এ লেখা — এই দুটি ঘটনা জনপ্রিয়তা বাড়াতে বড় ভূমিকা রাখে। অন্য চেষ্টাগুলো (Closure Compiler, Dart, Flow) নিয়ে ভাবনার প্রশ্ন ১-এ।

৩ · সেটআপ — tsc, tsconfig আর strict

TypeScript একটি npm package. প্রজেক্টের ভেতরে development dependency হিসেবে বসানোই প্রচলিত, যাতে দলের সবাই একই version পান (npm আর Vite নিয়ে বিস্তারিত পর্ব ৪৬-এ):

npm install --save-dev typescript   # প্রজেক্টে TypeScript বসানো
npx tsc --init                      # tsconfig.json তৈরি
npx tsc --noEmit                    # শুধু টাইপ যাচাই, কোনো ফাইল লেখা হবে না
npx tsc                             # যাচাই + .js ফাইল তৈরি

সব সেটিং থাকে tsconfig.json-এ। শুরুতে অনেক বিকল্প দেখে ভয় পাবেন না — আধুনিক প্রজেক্টে মূল কয়েকটিই যথেষ্ট:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

target ঠিক করে কোন JavaScript version-এর কোড বানাবে আর কোন built-in API চিনবে। module আর moduleResolution বলে import কীভাবে খোঁজা হবে (পর্ব ৪৫-এর ESM); "Bundler" Vite-এর মতো টুলের জন্য, শুধু Node.js-এর প্রজেক্টে সাধারণত "NodeNext". noEmit মানে tsc শুধু যাচাই করবে, ফাইল লিখবে না — কারণ Vite বা esbuild নিজেই টাইপ মুছে bundle বানায়, আর সেগুলো নিজে টাইপ যাচাই করে না. তাই এমন প্রজেক্টে package.json-এ "typecheck": "tsc --noEmit" script রাখা হয় আর CI-তে চালানো হয়। skipLibCheck বাইরের লাইব্রেরির টাইপ-ফাইল যাচাই বাদ দিয়ে সময় বাঁচায়।

মূল ধারণা

নতুন প্রজেক্টে প্রথম দিন থেকেই "strict": true. এটি একগুচ্ছ কড়া নিয়ম একসাথে চালু করে। সবচেয়ে গুরুত্বপূর্ণ দুটি: strictNullChecks — null আর undefined আলাদা টাইপ হয়, তাই document.querySelector("input")-এর ফল সরাসরি ব্যবহার করলে compiler বলে 'el' is possibly 'null'.; আর noImplicitAny — টাইপ বোঝা না গেলে চুপচাপ any না ধরে error দেয়: Parameter 'p' implicitly has an 'any' type. Strict ছাড়া TypeScript তার অর্ধেক শক্তি হারায়; পরে চালু করা অনেক কষ্টের।

.ts ফাইল সরাসরি চালানো — Node.js, tsx আর Vite

  • Vite (browser প্রজেক্ট): .ts ফাইল import করলেই চলে — Vite ভেতরে একটি দ্রুত transformer দিয়ে (Vite 8-এ Oxc, আগের সংস্করণে esbuild) টাইপ মুছে নেয়। টাইপ যাচাইয়ের জন্য editor আর tsc --noEmit.
  • Node.js-এর type stripping: Node 22.6-এ --experimental-strip-types flag দিয়ে এটি পরীক্ষামূলকভাবে আসে; Node 23.6 থেকে flag ছাড়াই চালু, আর পরে Node 22-এর আপডেটেও আসে। Node 24-এ node app.ts লিখলেই চলে। কিন্তু দুটি সীমা মনে রাখুন: এটি শুধু টাইপ মুছে দেয়, যাচাই করে না; আর যেসব TypeScript syntax-এর জন্য সত্যিকারের JavaScript কোড বানাতে হয় (যেমন enum), তা strip-only mode-এ চলে না — Node error দেয়: TypeScript enum is not supported in strip-only mode.
  • tsx বা ts-node: পুরোনো Node version-এ বা সব syntax-সহ চালাতে npx tsx app.ts. tsx-ও (esbuild দিয়ে) টাইপ যাচাই করে না, শুধু মোছে।

লক্ষ করুন একটি ধরন: আধুনিক টুলচেইনে “চালানো” আর “যাচাই করা” আলাদা কাজ. চালানো দ্রুত হয় কারণ টাইপ শুধু মুছে ফেলা হয়; যাচাই করে editor আর CI-তে tsc. এই ভাগাভাগির কারণে TypeScript 5.8-এ erasableSyntaxOnly নামে একটি সেটিং যোগ হয়েছে, যা শুধু “মুছে ফেলা যায়” এমন syntax-এর অনুমতি দেয় — enum, namespace বা constructor-এর parameter property নিষেধ করে।

৪ · মৌলিক টাইপ, inference আর any / unknown / never

TypeScript-এর মৌলিক টাইপগুলো JavaScript-এর primitive-গুলোর সাথেই মেলে (পর্ব ০৬) — শুধু ছোট হাতের অক্ষরে লেখা:

টাইপউদাহরণমনে রাখুন
string, number, booleanlet city: string = "ঢাকা";String, Number (বড় হাতের) নয় — ওগুলো wrapper object-এর টাইপ
bigintlet id: bigint = 9007199254740993n;target ES2020 বা নতুন লাগে
null, undefinedlet phone: string | null = null;strict-এ আলাদা টাইপ — “খালি হতে পারে” কথাটা স্পষ্ট লিখতে হয়
arraynumber[] বা Array<number>দুটি লেখা একই অর্থ
tuplelet point: [number, number] = [23.8, 90.4];নির্দিষ্ট দৈর্ঘ্য, প্রতিটি ঘরের নিজস্ব টাইপ
voidfunction log(msg: string): voidfunction কোনো দরকারি মান ফেরত দেয় না

Type inference — সব কিছুতে টাইপ লিখবেন না

নতুনরা প্রায়ই প্রতিটি variable-এ টাইপ লেখেন: const count: number = 5;. দরকার নেই। Type inference মানে compiler নিজেই মান দেখে টাইপ বের করে: const count = 5-এ টাইপ 5 (একটি literal টাইপ, অংশ ৬), let total = 0-এ number, const names = ["Rafi", "Mim"]-এ string[]. Function-এর ফেরত টাইপও বের হয়ে যায়। VS Code-এ যেকোনো নামের ওপর মাউস রাখলে inferred টাইপ দেখায়।

প্রচলিত নিয়ম: সীমানায় টাইপ লিখুন, ভেতরে inference-এর ওপর ছাড়ুন। সীমানা মানে function-এর parameter (এদের টাইপ কখনো inference হয় না), বাইরে export করা function-এর ফেরত টাইপ, আর বাইরে থেকে আসা ডেটার আকার। ফাঁকা array বা পরে মান বসবে এমন variable-এও টাইপ লাগে: const items: Item[] = [];.

any, unknown আর never — তিন বিশেষ টাইপ

  • any — “এখানে টাইপ-যাচাই বন্ধ”. যেকোনো মান বসে, যেকোনো কিছু করা যায়, আর এটি যেখানে যায় সেখানেও যাচাই বন্ধ করে দেয়। এটি TypeScript-এর ভেতরে একটি JavaScript-এর জানালা।
  • unknown — “কী আছে জানি না, তাই আগে দেখে নাও”. যেকোনো মান বসে, কিন্তু যাচাই না করে প্রায় কিছুই করা যায় না। বাইরের ডেটার জন্য এটিই সঠিক টাইপ।
  • never — “এমন মান কখনো থাকতে পারে না”. যে function সবসময় throw করে, তার ফেরত টাইপ; আর সব সম্ভাবনা বাদ যাওয়ার পর যা থাকে (অংশ ৬)।

JSON.parse-এর ফেরত টাইপ any — TypeScript-এর ইতিহাসের একটি আপস। নিচে একই ডেটা দুইভাবে নিলে কী হয়, ধাপে ধাপে দেখুন (strict mode ধরে):

সার্ভার পাঠাল: {"name": 42} — name হওয়ার কথা ছিল string
const a: any = JSON.parse(s)
const u: unknown = JSON.parse(s)
  1. শুরু. একই JSON, দুটি টাইপ। any বলে “আমাকে বিশ্বাস করো”; unknown বলে “আমি কিছু জানি না”.
  2. ব্যবহার করতে গেলে. any-এর ওপর .name.toUpperCase() লিখলে compiler কিছুই বলে না। unknown-এর ওপর property পড়তেই error: 'u' is of type 'unknown'. (strict mode-এর বার্তা)।
  3. যাচাই. unknown আপনাকে বাধ্য করে আগে দেখে নিতে: object কি না, name ঘর আছে কি না, সেটি string কি না। যাচাইয়ের পর compiler টাইপ সংকুচিত করে নেয় (অংশ ৬)।
  4. ভুল ডেটা এলে. any-এর পথে প্রোগ্রাম গভীরে গিয়ে ভেঙে পড়ে — V8-এর বার্তা TypeError: a.name.toUpperCase is not a function, কোথা থেকে ভুল এসেছে বোঝা কঠিন। unknown-এর পথে যাচাইয়ের জায়গাতেই আপনার লেখা পরিষ্কার error.
  5. শিক্ষা. any সমস্যা লুকায়, unknown সমস্যা সামনে আনে। বাইরের ডেটা (API, JSON.parse, localStorage, catch-এর error) সবসময় unknown হিসেবে নিন। Strict mode-এ catch (err)-এর err-ও ডিফল্টভাবে unknown.

৫ · function আর object-এর টাইপ — type, interface আর structural typing

function-এর টাইপ

function price(base: number, vatRate = 0.15, coupon?: string): number {
  const withVat = base * (1 + vatRate);          // vatRate: number (default থেকে inferred)
  return coupon === "EID10" ? withVat * 0.9 : withVat;   // coupon: string | undefined
}

const notify = (msg: string): void => console.log(msg);
type Formatter = (amount: number) => string;      // function-এর টাইপ
const toTaka: Formatter = n => `৳${n.toFixed(2)}`; // n-এর টাইপ লিখতে হয়নি

coupon?-এর ? মানে argument না দিলেও চলবে — তখন মান undefined, তাই ভেতরে টাইপ string | undefined. Default মান থাকলে (vatRate = 0.15) টাইপ সেখান থেকেই বের হয়, আর সেটিও ঐচ্ছিক। price("500") লিখলে error: Argument of type 'string' is not assignable to parameter of type 'number'. শেষ লাইনে Formatter টাইপ থেকে compiler জানে n একটি number — একে বলে contextual typing. (Function, default আর rest parameter-এর মূল আলোচনা পর্ব ১৩-এ।)

object-এর টাইপ: type alias আর interface

Object-এর আকার বর্ণনার দুটি উপায় আছে। type alias যেকোনো টাইপকে একটি নাম দেয়; interface শুধু object-এর আকার বর্ণনা করে:

type Product = {
  readonly id: number;          // একবার বসানোর পর বদলানো যাবে না
  name: string;
  price: number;
  discount?: number;            // ঐচ্ছিক ঘর
};

interface Customer {
  name: string;
  phone: string | null;
}

type StockByShop = Record<string, number>;     // { [shop: string]: number }-এর ছোট রূপ
const stock: StockByShop = { mirpur: 12, uttara: 0 };

readonly ঘরে নতুন মান বসাতে গেলে: Cannot assign to 'id' because it is a read-only property. মনে রাখবেন, এটিও শুধু compile-এর সময়ের নিয়ম — চলার সময় Object.freeze-এর মতো কিছু ঘটে না। Record<string, number> হলো একটি index signature: “যেকোনো string key, প্রতিটির মান number”. Key আগে থেকে জানা না থাকলে (দোকানের নাম, বাংলা শব্দের গণনা) এটি কাজে লাগে। Key-গুলো একটি নির্দিষ্ট তালিকা হলে আরও কড়া করে লিখুন: Record<"mirpur" | "uttara", number>. আর key ঘন ঘন যোগ-বিয়োগ হলে Map বিবেচনা করুন (পর্ব ১৮)।

type না interface? Object-এর জন্য দুটিই প্রায় একই কাজ করে। পার্থক্য কয়েকটি: interface extends দিয়ে বাড়ানো যায় আর একই নামে দুবার লিখলে মিশে যায় (declaration merging — লাইব্রেরির টাইপ বাড়াতে কাজের); type দিয়ে union, tuple, function টাইপসহ সবকিছুর নাম দেওয়া যায়। ব্যবহারিক নিয়ম: দলে যেকোনো একটি ধারাবাহিকভাবে ব্যবহার করুন; union দরকার হলে type.

Structural typing — নাম নয়, আকার দেখে মেলানো

Java বা C#-এ একটি class অন্য টাইপের জায়গায় বসে শুধু তখনই, যখন সে স্পষ্ট করে implements বা extends লেখে — একে বলে nominal typing (নাম দেখে)। TypeScript ব্যবহার করে structural typing: কোনো মানের মধ্যে প্রয়োজনীয় সব ঘর ঠিক টাইপে থাকলেই সে মেলে, নাম যাই হোক। JavaScript-এর জগতে object প্রায়ই literal বা JSON থেকে তৈরি হয়, কোনো class থেকে নয় — তাই এই নকশা। ধাপে ধাপে দেখুন:

interface Point { x: number; y: number }
কোড
ফল
কেন
  1. আকার মিললেই চলে. a-কে কেউ Point বলেনি, কিন্তু তার x আর y আছে, দুটোই number — তাই সে Point-এর জায়গায় বসে। Java-তে এমন হতো না।
  2. বাড়তি ঘর সমস্যা নয়. b-তে একটি বাড়তি label আছে। Point-এর যা দরকার, সবই আছে — বাকিটা অপ্রাসঙ্গিক। “Point হলো এমন সব কিছু যার অন্তত x আর y আছে।”
  3. ব্যতিক্রম: নতুন object literal. সরাসরি { … } লিখে Point-এ বসালে বাড়তি ঘর error দেয়। একে বলে excess property check. যুক্তি: এখানে বাড়তি ঘর অন্য কোথাও ব্যবহারের সুযোগ নেই, তাই এটি প্রায় নিশ্চিত বানান ভুল (colour বনাম color, ঐচ্ছিক ঘরের ভুল নাম)।
  4. Variable হয়ে এলে আবার ঠিক. show(b) চলে, কারণ b একটি আগে থেকে থাকা object — তার বাড়তি ঘর অন্য কাজে লাগতে পারে। Excess property check শুধু “fresh” literal-এর জন্য।
  5. কম থাকলে কখনোই নয়. y নেই এমন object কোনো অবস্থাতেই Point নয়। নিয়মটা সংক্ষেপে: দরকারি সব ঘর থাকতে হবে; বাড়তি ঘর চলে, শুধু নতুন literal ছাড়া।

Structural typing-এর একটি ফাঁদও আছে: type UserId = number আর type OrderId = number — দুটি আসলে একই টাইপ, তাই ভুল করে order-এর আইডি user-এর জায়গায় দিলে compiler ধরে না। দরকার হলে “brand” কৌশল (number & { __brand: "UserId" }) দিয়ে nominal আচরণ নকল করা যায় — ভাবনার প্রশ্ন ২-এ এর পেছনের তত্ত্ব।

৬ · union, narrowing আর discriminated union

বাস্তব JavaScript-এ একটি মান প্রায়ই কয়েক রকমের হতে পারে: ফোন নম্বর string বা null; একটি id number বা string. Union type এটিই প্রকাশ করে: string | number মানে “string অথবা number”. কিন্তু union-এর ওপর শুধু সেই কাজগুলো করা যায়, যা সব সদস্যের জন্য বৈধ — (id: string | number)-এর ওপর id.toFixed() লিখলে error: Property 'toFixed' does not exist on type 'string | number'.

সমাধান narrowing: আপনি সাধারণ JavaScript-এর শর্ত লেখেন, আর compiler সেই শর্ত পড়ে প্রতিটি শাখায় টাইপ সংকুচিত করে নেয়। নতুন কোনো syntax লাগে না — এটাই TypeScript-এর সবচেয়ে চতুর নকশা:

function formatId(id: string | number): string {
  if (typeof id === "number") {
    return id.toFixed(0);        // এখানে id: number
  }
  return id.toUpperCase();       // এখানে id: string — number আগেই বেরিয়ে গেছে
}

function contact(c: { phone: string } | { email: string }): string {
  return "phone" in c ? `ফোন: ${c.phone}` : `ইমেইল: ${c.email}`;   // in দিয়ে narrowing
}

typeof, in, instanceof, === দিয়ে তুলনা, Array.isArray, এমনকি truthiness (if (phone)) — সবই narrowing ঘটায়। এগুলোকে বলে type guard. নিজের guard-ও লেখা যায়: function isProduct(x: unknown): x is Product — অংশ ৮-এ দেখবেন।

Literal type আর as const

টাইপ শুধু “যেকোনো string” নয়, একটি নির্দিষ্ট মানও হতে পারে: "paid" টাইপে শুধু "paid" লেখাটিই বসে। এগুলো literal type, আর এদের union দিয়ে বানানো যায় একটি নির্দিষ্ট তালিকা: type Status = "pending" | "paid" | "shipped". এখন "shiped" লিখলে error: Type '"shiped"' is not assignable to type 'Status'. Did you mean '"shipped"'? — বানান ভুল চালানোর আগেই ধরা পড়ল, সাথে সঠিক বানানের পরামর্শও।

একটি সূক্ষ্মতা: const s = "paid"-এর টাইপ "paid", কিন্তু let s = "paid"-এর টাইপ string — কারণ let পরে বদলাতে পারে (একে বলে widening). Object আর array-র ঘরও widen হয়। পুরো মানকে অপরিবর্তনীয় literal হিসেবে রাখতে আছে as const:

const STATUSES = ["pending", "paid", "shipped"] as const;
type Status = (typeof STATUSES)[number];   // "pending" | "paid" | "shipped"

const config = { mode: "dark", retries: 3 } as const;
// config.mode = "light";  → Cannot assign to 'mode' because it is a read-only property.

as const তিনটি কাজ করে: সব ঘর readonly, array হয় readonly tuple, আর প্রতিটি মান তার literal টাইপ পায়। দ্বিতীয় লাইনের কৌশলটি খুব প্রচলিত: একটি সত্যিকারের array (যা চলার সময়ও আছে, dropdown বানাতে বা যাচাইয়ে লাগে) থেকে টাইপ বের করা — তালিকা একটাই, তাই দুটো আলাদা করে মেলানোর ঝামেলা নেই।

Discriminated union — অবস্থা দিয়ে ডেটা চেনা

অংশ ২-এর Order-এ ফিরে যাই। একটি অর্ডারের তিনটি অবস্থা, আর প্রতিটি অবস্থায় আলাদা তথ্য থাকে: paid হলে পেমেন্টের তারিখ, shipped হলে কুরিয়ারের নাম। সব ঘর ঐচ্ছিক করে একটি টাইপ বানালে (paidAt?, courier?) অসম্ভব অবস্থাও বৈধ হয়ে যায় — “pending, অথচ কুরিয়ার আছে”. তার বদলে প্রতিটি অবস্থার আলাদা টাইপ বানিয়ে union করা হয়, আর সবার মধ্যে থাকে একটি সাধারণ literal ঘর (status) — যাকে বলে discriminant. একেই বলে discriminated union. নিচে compiler-এর চোখে প্রতিটি লাইনে o-এর টাইপ দেখুন:

1function label(o: Order): string {2  if (o.status === "pending") {3    return `#${o.id} অপেক্ষায়`;4  }5  switch (o.status) {6    case "paid":    return o.paidAt;7    case "shipped": return o.courier;8    default:        return assertNever(o);9  }10}
Compiler-এর চোখে
o-এর টাইপ
পড়া যায়
  1. শুরুতে পুরো union. o তিন অবস্থার যেকোনোটি। তাই শুধু সবার মধ্যে থাকা ঘর পড়া যায়: status আর id. এখানে o.courier লিখলে error.
  2. Discriminant-এর তুলনা. o.status === "pending" একটি type guard. status literal টাইপ বলে compiler জানে, এই শর্ত সত্য হলে কোন সদস্য।
  3. if-এর ভেতরে. এখানে o শুধু pending অবস্থা। শাখাটি return দিয়ে শেষ হয় — compiler এটাও মনে রাখে।
  4. if-এর পরে. pending অবস্থা আগেই বেরিয়ে গেছে, তাই এখন o হলো paid | shipped. একে বলে control-flow analysis — compiler কোডের প্রবাহ ধরে টাইপ হিসাব করে।
  5. case "paid". টাইপ আরও সংকুচিত — শুধু paid. এখন o.paidAt নিরাপদ।
  6. case "shipped". শুধু shipped, তাই o.courier বৈধ।
  7. default — never. তিনটি অবস্থাই সামলানো হয়েছে, তাই এখানে o-এর টাইপ never. assertNever(o: never)-কে ডাকা তাই বৈধ। কাল কেউ Order-এ "cancelled" যোগ করলে এই লাইনে error আসবে — Argument of type '{ status: "cancelled"; id: number; }' is not assignable to parameter of type 'never'. একে বলে exhaustiveness check.
function assertNever(x: never): never {
  throw new Error("অপ্রত্যাশিত মান: " + JSON.stringify(x));
}

এই ছোট function দুই স্তরের নিরাপত্তা দেয়। Compile-এর সময়: কোনো case বাদ পড়লে argument আর never থাকে না, তাই error. চলার সময়: সার্ভার যদি তালিকার বাইরের কোনো status পাঠায় (টাইপ তো তখন মুছে গেছে!), চুপচাপ undefined ফেরত না দিয়ে পরিষ্কার error দেয়। switch আর case নিয়ে মূল আলোচনা পর্ব ১১-এ।

মূল ধারণা

“অসম্ভব অবস্থাকে অপ্রকাশযোগ্য করুন” (make illegal states unrepresentable). টাইপ এমনভাবে বানান যাতে ভুল অবস্থা লেখাই না যায়। Loading/success/error-ওয়ালা যেকোনো ডেটা — API কল, form জমা, পেমেন্ট — discriminated union দিয়ে লিখলে if (data && !error && !loading) জাতীয় জট খুলে যায়।

৭ · generics, utility type আর as / ! — শক্তি ও বিপদ

Generics — টাইপের parameter

একটি function চাই যা যেকোনো array-র প্রথম উপাদান দেয়। (arr: any[]): any লিখলে টাইপের তথ্য হারায় — ফলের টাইপ কী, কেউ জানে না। Generics দিয়ে টাইপকেও parameter বানানো যায়:

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([10, 20]);        // n: number | undefined
const s = first(["চা", "কফি"]);   // s: string | undefined

function groupBy<T, K extends PropertyKey>(items: T[], keyOf: (item: T) => K): Record<K, T[]> {
  const out = {} as Record<K, T[]>;
  for (const item of items) {
    (out[keyOf(item)] ??= []).push(item);
  }
  return out;
}
const byArea = groupBy(orders, o => o.area);   // Record<string, Order[]>

T একটি টাইপ-variable: first([10, 20]) ডাকলে compiler নিজেই T = number ধরে নেয় — টাইপ লিখে দিতে হয় না। ফেরত টাইপ T | undefined, কারণ খালি array-তে প্রথম উপাদান নেই — সৎ টাইপ। groupBy-এ দুটি টাইপ-parameter; K extends PropertyKey একটি শর্ত: K অবশ্যই এমন কিছু যা object-এর key হতে পারে (string, number বা symbol). এটি পর্ব ১৬-এর reduce-ভিত্তিক groupBy-এর টাইপসহ রূপ; নতুন JavaScript-এ Object.groupBy-ও আছে। ভেতরের {} as Record<K, T[]> একটি সীমিত, বোঝাপড়ার as — খালি object শুরুতে Record নয়, কিন্তু লুপ শেষে হবে।

Generics আপনি প্রতিদিনই ব্যবহার করছেন: Array<Order>, Map<string, number>, আর async function-এর ফেরত টাইপ Promise<Product> — “একটি Promise, যা পূর্ণ হলে Product দেবে” (পর্ব ৩৩–৩৪)। নিয়ম: নিজের কোডে generics লিখুন যখন একই যুক্তি বিভিন্ন টাইপে খাটে আর ইনপুট-আউটপুটের টাইপের সম্পর্ক রাখা জরুরি। পাঁচ স্তরের জটিল conditional type সাধারণ অ্যাপ কোডে প্রায় কখনো লাগে না।

Utility type — টাইপ থেকে নতুন টাইপ

Utility typeযা করেবাস্তব ব্যবহার
Partial<Product>সব ঘর ঐচ্ছিকedit form-এর খসড়া, PATCH request
Pick<Product, "name" | "price">শুধু বাছাই করা ঘরতালিকার ছোট কার্ড
Omit<Product, "id">কিছু ঘর বাদনতুন পণ্য তৈরি — id এখনো নেই
Readonly<Product>সব ঘর readonlystate যা সরাসরি বদলানো যাবে না
Record<K, V>K key, V মানlookup table, groupBy-এর ফল
ReturnType<typeof fn>function-এর ফেরত টাইপলাইব্রেরির function, যার টাইপের নাম export করা নেই
Awaited<T>Promise খুলে ভেতরের টাইপAwaited<ReturnType<typeof loadProduct>>

এগুলোকে বলে utility type — TypeScript-এর সাথে আসা তৈরি generic টাইপ। মূল সুবিধা: একটিই উৎস (Product) থেকে সব রূপ বের হয়। Product-এ নতুন ঘর যোগ করলে সব রূপ নিজে থেকে হালনাগাদ হয়।

as আর ! — compiler-কে চুপ করানোর অস্ত্র

Type assertion (value as Type) মানে compiler-কে বলা: “হিসাব যা-ই বলুক, এটি এই টাইপ — মেনে নাও”. এটি কোনো রূপান্তর বা যাচাই করে না — চলার সময় পুরোপুরি মুছে যায়। const user = JSON.parse(text) as User; লিখলে compiler সন্তুষ্ট, কিন্তু JSON-এ যা-ই থাকুক, কেউ দেখল না। বাকি সব কোড এখন একটি মিথ্যা টাইপের ওপর দাঁড়িয়ে।

Non-null assertion (!) হলো ছোট রূপ: document.querySelector("input")! মানে “এটি null হবে না, আমি নিশ্চিত”. HTML-এ element না থাকলে চলার সময় সেই পুরোনো TypeError: Cannot read properties of null.

সন্দেহের তালিকা: কোড রিভিউতে প্রতিটি as, !, any আর // @ts-ignore একটি প্রশ্ন: “এখানে compiler-এর চেয়ে আমি বেশি জানি — তার প্রমাণ কী?” সঠিক বিকল্প প্রায়ই narrowing (if (!el) throw …) বা যাচাই। TypeScript 4.9 থেকে আছে satisfies: const routes = {…} satisfies Record<string, Route> — টাইপ মেলে কি না যাচাই করে, কিন্তু inferred টাইপ নষ্ট করে না; as-এর নিরাপদ বিকল্প হিসেবে এটি শিখে রাখুন।

৮ · বাস্তব প্রজেক্টে — API ডেটা, class, enum, .d.ts আর ধীরে ধীরে migration

API-র ডেটা — টাইপ মুছে যায়, তাই যাচাই চাই

পর্ব ৩৫-এ fetch দেখেছেন। res.json()-এর ফেরত টাইপ Promise<any> — অর্থাৎ সার্ভার থেকে আসা সব কিছু চুপচাপ যাচাইহীন। সার্ভার, অন্য দল, পুরোনো app version — কেউই আপনার টাইপ মানতে বাধ্য নয়। সঠিক পথ: সীমানায় unknown, তারপর যাচাই, তারপর ভেতরে নিশ্চিন্তে টাইপ।

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

function isProduct(x: unknown): x is Product {
  return typeof x === "object" && x !== null
    && "id" in x && typeof x.id === "number"
    && "name" in x && typeof x.name === "string"
    && "price" in x && typeof x.price === "number";
}

async function loadProduct(id: number): Promise<Product> {
  const res = await fetch(`/api/products/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data: unknown = await res.json();
  if (!isProduct(data)) throw new Error("সার্ভারের ডেটা প্রত্যাশিত আকারে নেই");
  return data;                       // এখানে data: Product
}

x is Product হলো একটি নিজের লেখা type guard: function true দিলে compiler ধরে নেয় x একটি Product. কোডটি strict mode-এ tsc 5.6 দিয়ে পরিষ্কারভাবে যাচাই করা। কিন্তু খেয়াল করুন — guard-এর ভেতরের যুক্তি ভুল হলেও (ধরুন price-এর পরীক্ষা ভুলে গেলেন) compiler আপত্তি করবে না; type guard-ও একধরনের প্রতিশ্রুতি। বড় ডেটার জন্য হাতে লেখা guard ক্লান্তিকর, তাই প্রচলিত schema validation লাইব্রেরি — যেমন Zod:

import { z } from "zod";

const ProductSchema = z.object({
  id: z.number().int(),
  name: z.string().min(1),
  price: z.number().nonnegative(),
});
type Product = z.infer<typeof ProductSchema>;   // টাইপ schema থেকেই বের হলো

const product = ProductSchema.parse(await res.json());   // ভুল হলে স্পষ্ট ZodError

এটি চালাতে npm install zod লাগে, তাই এখানে চালানো যাবে না। মূল ধারণা: schema চলার সময়ও আছে (যাচাই করে), আর টাইপ সেই schema থেকে বের হয় — দুটো কখনো আলাদা হয়ে যায় না। Form-এর ইনপুট (পর্ব ৩৯) আর সার্ভারের কাছে আসা request (পর্ব ৫০-এ Express) — দুই সীমানাতেই একই schema ব্যবহার করা যায়।

TypeScript-এ class — সংক্ষেপে

পর্ব ২৬-এর class TypeScript-এ একইভাবে চলে, সাথে যোগ হয় ঘরের টাইপ আর access modifier: public, private, protected. এখানে একটি গুরুত্বপূর্ণ পার্থক্য আছে:

class Account {
  private balance = 0;          // শুধু compile-এর সময় লুকানো
  #pin: string;                 // JavaScript-এর আসল private — চলার সময়ও লুকানো
  constructor(readonly owner: string, pin: string) { this.#pin = pin; }
  deposit(amount: number): void { this.balance += amount; }
}

acc.balance বাইরে থেকে পড়লে compiler error দেয় (Property 'balance' is private and only accessible within class 'Account'.), কিন্তু টাইপ মুছে যাওয়ার পর এটি একটি সাধারণ property — acc["balance"] বা console-এ সহজেই দেখা যায়। #private (ES2022) engine নিজে রক্ষা করে। সত্যিকারের গোপনীয়তা চাইলে #. constructor(readonly owner: string) হলো “parameter property” — ঘর ঘোষণা আর মান বসানো এক সাথে; এটি সুবিধাজনক, কিন্তু মুছে ফেলা যায় না (JavaScript কোড বানাতে হয়), তাই erasableSyntaxOnly-এ নিষেধ।

enum বনাম union literal

TypeScript-এর enum (enum Status { Pending, Paid }) JavaScript-এ নেই — compiler একটি সত্যিকারের object বানায়। এর কিছু অস্বস্তি আছে: সংখ্যাভিত্তিক enum-এ যেকোনো সংখ্যা বসে যায় (পুরোনো version-এ), চলার সময় বাড়তি কোড থাকে, আর Node.js-এর type stripping-এ চলে না। আধুনিক পরামর্শ: union literal পছন্দ করুন — type Status = "pending" | "paid", দরকার হলে as const array বা object-এর সাথে। এটি শুধু টাইপ, মুছে ফেলা যায়, আর JSON-এ যা আসে ("paid") তার সাথে সরাসরি মেলে। পুরোনো কোডবেসে enum দেখলে ভয়ের কিছু নেই; এটি নিষিদ্ধ নয়, শুধু নতুন কোডে কম ব্যবহৃত হচ্ছে।

.d.ts আর @types — JavaScript লাইব্রেরির টাইপ

npm-এর অনেক লাইব্রেরি সাধারণ JavaScript-এ লেখা। তাদের টাইপ বর্ণনা থাকে declaration file-এ (.d.ts) — শুধু টাইপ, কোনো কোড নেই। অনেক লাইব্রেরি নিজেই .d.ts সাথে দেয় (Zod, Vite). যারা দেয় না, তাদের জন্য সম্প্রদায় চালিত DefinitelyTyped প্রকল্প @types/… package বানায়: npm i -D @types/node, @types/express. Node.js-এর process বা fs চিনতেও @types/node লাগে — না থাকলে error: Cannot find name 'process'. Do you need to install type definitions for node?

TypeScript ছাড়াই টাইপ: JSDoc আর // @ts-check

Build ধাপ যোগ করতে না চাইলে একটি মৃদু পথ আছে। সাধারণ .js ফাইলের প্রথম লাইনে // @ts-check লিখুন আর JSDoc comment-এ টাইপ দিন:

// @ts-check
/** @typedef {{ name: string, price: number, qty: number }} Item */

/** @param {Item[]} items  @param {number} delivery  @returns {number} */
function cartTotal(items, delivery) {
  return items.reduce((s, it) => s + it.price * it.qty, 0) + delivery;
}

ফাইলটি এখনো সাধারণ JavaScript — browser সরাসরি চালায়। কিন্তু VS Code এখন TypeScript-এর engine দিয়ে এটি যাচাই করে, আর cartTotal(cart, "60")-এ লাল দাগ দেয়। Svelte-এর মতো কিছু বড় প্রকল্প তাদের লাইব্রেরির কোড এভাবেই JSDoc-এ টাইপ করে। শুরু করার, বা ছোট প্রজেক্টে টাইপের সুবিধা নেওয়ার চমৎকার উপায়।

পুরোনো JavaScript প্রজেক্ট ধীরে ধীরে TypeScript-এ

  1. পাশাপাশি চালান: tsconfig-এ "allowJs": true — .js আর .ts একসাথে থাকে; কাউকে সব একদিনে বদলাতে হয় না।
  2. সীমানা থেকে শুরু করুন: আগে ডেটার টাইপ (API response, Order, User) আর সবচেয়ে বেশি ব্যবহৃত utility ফাইলগুলো .ts করুন — সেখানে লাভ সবচেয়ে বেশি।
  3. কড়াকড়ি ধাপে ধাপে বাড়ান: শুরুতে strict: false চললে, এরপর noImplicitAny, তারপর strictNullChecks, শেষে পুরো strict. প্রতিটি ধাপে error-এর সংখ্যা কমিয়ে শূন্যে আনুন।
  4. any-কে ঋণ হিসেবে দেখুন: সাময়িক any চলতে পারে, কিন্তু গুনে রাখুন; typescript-eslint-এর no-explicit-any নিয়ম (ESLint নিয়ে পর্ব ৪৬-এ) নতুন any ঢোকা আটকায়।

Error বার্তা পড়ার কৌশল

TypeScript-এর লম্বা error ভয় দেখায়, কিন্তু গঠন সবসময় এক: “Type A is not assignable to type B”, তারপর ইন্ডেন্ট করা লাইনে কেন. যেমন Type 'number | undefined' is not assignable to type 'number'.-এর নিচে লেখা থাকে Type 'undefined' is not assignable to type 'number'. — সবচেয়ে ভেতরের (শেষ) লাইনটি আসল কারণ। বাকিগুলো সেখানে পৌঁছানোর পথ। কয়েকটি পরিচিত কোড: TS2322 (টাইপ মেলে না), TS2345 (argument মেলে না), TS2339 (property নেই), TS18047 (null হতে পারে), TS7006 (implicit any). এই কোড দিয়ে সার্চ করলে ব্যাখ্যা সহজে পাওয়া যায়।

কখন TypeScript বাড়াবাড়ি?

এক-দুই ফাইলের script, একবার চালিয়ে ফেলে দেওয়ার কাজ, শেখার শুরুর দিন, বা ছোট পরীক্ষামূলক প্রোটোটাইপে TypeScript-এর খরচ লাভের চেয়ে বেশি হতে পারে — সেখানে // @ts-check যথেষ্ট। আবার বহুজনের, বহু মাসের, বা লাইব্রেরি হিসেবে অন্যরা ব্যবহার করবে এমন কোডে TypeScript এখন শিল্পের প্রায় ডিফল্ট। আর মনে রাখুন: TypeScript টেস্টের বদলি নয়। টাইপ বলে “price একটি number”; টেস্ট বলে “ছাড়ের পর price ঠিক ৪৫০” (পর্ব ৪৮)।

C/C++/Python/Java জানলে

Java আর C++-এর টাইপ nominal ও static — class-কে স্পষ্ট implements করতে হয়, আর টাইপ চলার সময়ও কিছুটা থাকে (Java-র instanceof, reflection). TypeScript structural, আর টাইপ পুরোপুরি মুছে যায়। মিলটা আছে Java-র generics-এ: সেখানেও type erasure — List<String> চলার সময় শুধু List (Java-র generics). Python-এর type hint আর mypy দর্শনে TypeScript-এর সবচেয়ে কাছে — ঐচ্ছিক টাইপ, আলাদা checker, interpreter টাইপ যাচাই করে না (Python-এর type hint আর mypy). C++-এর template আর TypeScript generics দেখতে এক রকম হলেও template প্রতিটি টাইপের জন্য আলাদা কোড বানায়, TypeScript কোনো কোডই বানায় না।

পরীক্ষা ও ইন্টারভিউতে
  • TypeScript আর JavaScript-এর সম্পর্ক কী? — TypeScript JavaScript-এর superset; টাইপ যোগ করে, compile-এর সময় যাচাই করে, তারপর টাইপ মুছে সাধারণ JavaScript বানায়।
  • any আর unknown-এর পার্থক্য? — দুটিতেই যেকোনো মান বসে; any-তে যাচাই ছাড়াই সব করা যায়, unknown-এ narrowing বা যাচাইয়ের আগে প্রায় কিছুই করা যায় না। বাইরের ডেটার জন্য unknown.
  • TypeScript কি চলার সময় টাইপ যাচাই করে? — না। টাইপ compile-এর পর মুছে যায়; চলার সময়ের যাচাইয়ের জন্য নিজের type guard বা Zod-এর মতো লাইব্রেরি লাগে।
  • type আর interface-এর পার্থক্য? — interface শুধু object-এর আকার, extends আর declaration merging সমর্থন করে; type যেকোনো টাইপের (union, tuple, function) নাম দিতে পারে।
  • Discriminated union কী? — একটি সাধারণ literal ঘর (যেমন status) থাকা কয়েকটি object টাইপের union; সেই ঘর পরীক্ষা করলে compiler সঠিক সদস্যে টাইপ সংকুচিত করে, আর never দিয়ে সব case সামলানো নিশ্চিত করা যায়।

৯ · AI-এর যুগে — any আর as দিয়ে error চাপা দেওয়া

AI টুল TypeScript খুব দ্রুত লেখে, আর একটি নির্দিষ্ট দুর্বলতা দেখায়: তার লক্ষ্য প্রায়ই “লাল দাগ চলে যাক”, “টাইপ সত্য হোক” নয়। Compiler error দেখলে সবচেয়ে ছোট সমাধান হলো as any বা ! — error চলে যায়, বাগ থেকে যায়, আর এখন সেটি লুকানো। আরও একটি সমস্যা: AI API-র response দেখে সুন্দর একটি interface লেখে, কিন্তু যাচাই লেখে না — ফলে টাইপ বাস্তব ডেটার সাথে মেলে কি না, কেউ জানে না।

AI-এর কোডে যা দেখবেনসমস্যাযা লেখা উচিত
(data as any).itemsযাচাই বন্ধ, ভুল ছড়িয়ে পড়েunknown + type guard বা Zod
await res.json() as User[]টাইপ লেখা, যাচাই নেই — চলার সময় টাইপ নেইschema দিয়ে parse
getElementById("x")! সব জায়গায়element না থাকলে চলার সময় TypeErrorএকবার যাচাই করে স্পষ্ট error
// @ts-ignoreলাইনের সব error চিরতরে চাপাকারণ ঠিক করুন; একান্ত দরকারে @ts-expect-error আর কারণ লিখুন
সব জায়গায় enum আর classঅন্য ভাষার অভ্যাস; বাড়তি runtime কোডunion literal আর সাধারণ object টাইপ
পাঁচ স্তরের conditional/mapped genericপড়া যায় না, error বোঝা যায় নাসরল টাইপ; প্রয়োজনে কয়েকটি স্পষ্ট টাইপ
status?: string আর ঐচ্ছিক ঘরের স্তূপঅসম্ভব অবস্থাও বৈধdiscriminated union

AI-কে TypeScript কোড লিখতে বললে নিয়মগুলো আগেই প্রম্পটে বেঁধে দিন:

একটি TypeScript function লিখে দাও যা /api/orders থেকে অর্ডারের তালিকা এনে প্রতিটির ডেলিভারি-বার্তা ফেরত দেবে। শর্ত: ১. tsconfig-এ strict: true ধরে লিখবে। any, as (as const ছাড়া), ! আর @ts-ignore ব্যবহার করবে না। ২. res.json()-এর ফলকে unknown হিসেবে নেবে এবং Zod schema দিয়ে যাচাই করবে; টাইপ schema থেকে z.infer দিয়ে বের করবে। ৩. Order-এর অবস্থা (pending, paid, shipped) একটি discriminated union হবে, status ঘর দিয়ে; enum ব্যবহার করবে না। ৪. switch-এ সব অবস্থা সামলাবে আর default-এ never দিয়ে exhaustiveness check রাখবে। ৫. HTTP error আর যাচাই-ব্যর্থতার জন্য আলাদা, পরিষ্কার error বার্তা দেবে। ৬. শেষে বলবে কোন ৩টি ভুল ডেটা (যেমন price string হয়ে আসা) দিয়ে আমি নিজে পরীক্ষা করতে পারি।

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

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১JavaScript-এ টাইপ আনার চেষ্টা TypeScript-ই প্রথম নয় — Closure Compiler, Dart, Flow-ও ছিল। তাহলে TypeScript জিতল কেন?

২০০০-এর দশকের শেষে Gmail আর Google Maps-এর মতো বিশাল JavaScript অ্যাপ বানাতে গিয়ে Google ভেতরে তৈরি করে Closure Compiler, যা ২০০৯ সালে open source হয়। এতে টাইপ লেখা হতো JSDoc comment-এর ভেতরে (/** @type {number} */), আর compiler টাইপ যাচাইয়ের সাথে কোড ছোট ও দ্রুত করত। কাজ চমৎকার, কিন্তু লেখা কষ্টকর, আর টুলিং মূলত Google-এর নিজের পরিবেশের জন্য বানানো।

২০১১ সালে Google আরও সাহসী পথে যায়: Dart — নতুন ভাষা, পরিকল্পনা ছিল Chrome-এ আলাদা Dart VM দেওয়ার। অন্য browser নির্মাতারা আগ্রহ দেখায়নি, আর ২০১৫ সালে Google browser-এ Dart VM-এর পরিকল্পনা ছেড়ে দেয় (Dart পরে Flutter-এর ভাষা হিসেবে নতুন জীবন পায়)। শিক্ষা: ওয়েবে JavaScript-কে সরানো যায় না, শুধু তার ওপর গড়া যায়।

Microsoft ২০১২ সালে TypeScript আনে ঠিক এই উপলব্ধি নিয়ে: superset, যেকোনো JavaScript ফাইলের নাম বদলালেই TypeScript, আউটপুট পড়ার মতো সাধারণ JavaScript. ২০১৪ সালে Facebook আনে Flow — একই ধারণা, কিন্তু টাইপ-ব্যবস্থাকে আরও “sound” (গাণিতিকভাবে নির্ভুল) করার দিকে জোর। কয়েক বছর দুটি পাশাপাশি চলে।

TypeScript এগিয়ে যায় কয়েকটি কারণে। প্রথমত, টুলিং: compiler-এর সাথে একটি language service, যা editor-কে autocomplete, rename, go-to-definition দেয় — আর ২০১৫ সালের VS Code সেটিই ঘরে ঘরে পৌঁছে দেয়। দ্বিতীয়ত, DefinitelyTyped: সম্প্রদায় হাজার হাজার JavaScript লাইব্রেরির টাইপ লিখে দেয়, ফলে প্রথম দিন থেকেই jQuery বা Express-এর সাথে কাজ করা যেত। তৃতীয়ত, Angular: ২০১৫ সালে Google-এর Angular দল নিজেদের AtScript প্রকল্প ছেড়ে TypeScript গ্রহণ করে — প্রতিদ্বন্দ্বী কোম্পানির এই সমর্থন বড় সংকেত ছিল। চতুর্থত, বাস্তববাদ: TypeScript ইচ্ছা করেই পুরোপুরি sound নয়; তার নকশার লক্ষ্যের তালিকায় স্পষ্ট লেখা যে নির্ভুলতা আর উৎপাদনশীলতার মধ্যে ভারসাম্যই লক্ষ্য। Flow-এর কড়াকড়ি তত্ত্বে ভালো, কিন্তু বাস্তব JavaScript-এর অগোছালো ধরনের সাথে মানাতে বেশি কষ্ট দিত; আর তার উন্নয়ন মূলত Facebook-এর নিজের প্রয়োজনে চলত।

মূল উপলব্ধি: প্রযুক্তি শুধু ভালো ধারণায় জেতে না — জেতে বিদ্যমান ব্যবস্থার সাথে মানানসই হয়ে, ধীরে গ্রহণের সুযোগ দিয়ে, আর ব্যবহারকারীর প্রতিদিনের অভিজ্ঞতা (editor) উন্নত করে। “পুরোনোকে বদলাও” কৌশল প্রায়ই হারে “পুরোনোর ওপর গড়ো” কৌশলের কাছে।

প্র ০২Structural বনাম nominal typing, আর “gradual typing” — TypeScript-এর টাইপ-ব্যবস্থার পেছনের তত্ত্ব কী, আর কেন এটি ইচ্ছা করে “unsound”?

টাইপ-তত্ত্বে দুটি টাইপ “সামঞ্জস্যপূর্ণ” কি না, তা ঠিক করার দুটি প্রধান দর্শন আছে। Nominal ব্যবস্থায় (Java, C#, C++-এর class) নাম আর ঘোষিত সম্পর্কই সব: class Taka আর class Dollar-এর ভেতরে হুবহু একই ঘর থাকলেও একটি অন্যটির জায়গায় বসবে না, যতক্ষণ না কেউ স্পষ্ট করে সম্পর্ক ঘোষণা করে। Structural ব্যবস্থায় (TypeScript, Go-র interface, OCaml-এর object) আকারই সব — প্রয়োজনীয় ঘর থাকলেই চলে। Dynamic ভাষার “duck typing” (“হাঁসের মতো হাঁটলে আর ডাকলে সেটি হাঁস”) এরই চলার-সময়ের রূপ।

TypeScript structural বেছে নিয়েছে কারণ JavaScript-এর বাস্তবতা তাই। Object literal, JSON, অন্য লাইব্রেরি থেকে আসা object — এদের কোনো class-নাম নেই। Nominal ব্যবস্থা হলে প্রায় প্রতিটি JavaScript লাইব্রেরি ব্যবহারে ঝামেলা হতো। মূল্য হিসেবে আসে অংশ ৫-এর সমস্যা: UserId আর OrderId দুটোই number হলে আলাদা করা যায় না; তখন “branded type” দিয়ে কৃত্রিমভাবে nominal আচরণ আনতে হয়।

দ্বিতীয় তত্ত্বটি হলো gradual typing. ২০০৬ সালে Jeremy Siek আর Walid Taha “Gradual Typing for Functional Languages” প্রবন্ধে একটি কাঠামো দেন: একই প্রোগ্রামে টাইপসহ আর টাইপহীন অংশ থাকতে পারে, আর একটি বিশেষ “অজানা” টাইপ (TypeScript-এ যার ভূমিকা নেয় any) দুই জগতের মধ্যে সেতু। তাঁদের মূল ধারণায় সীমানায় চলার সময়ের যাচাই রাখার কথাও ছিল, যাতে টাইপহীন অংশ টাইপসহ অংশকে ঠকাতে না পারে। TypeScript সেই অংশ নেয়নি: সীমানায় কোনো runtime যাচাই নেই, টাইপ পুরোপুরি মুছে যায়। গবেষকেরা একে কখনো “optional typing” বলেন।

এর সাথে TypeScript আরও কিছু জায়গায় জেনেশুনে unsound: any, as, array-র covariance (Dog[]-কে Animal[] হিসেবে দিলে তাতে Cat ঢোকানো যায়), method parameter-এর bivariance, আর index দিয়ে পড়া (arr[5]-এর টাইপ undefined-ছাড়া, যতক্ষণ না noUncheckedIndexedAccess চালু করেন)। অর্থাৎ compiler “কোনো error নেই” বললেও চলার সময় টাইপ ভুল প্রমাণ হতে পারে। দল এটি গোপন করে না — নকশার “non-goal” তালিকায় লেখা আছে, প্রমাণযোগ্যভাবে নির্ভুল টাইপ-ব্যবস্থা লক্ষ্য নয়।

মূল উপলব্ধি: টাইপ-ব্যবস্থা একটি প্রকৌশলগত আপস। Soundness, প্রকাশক্ষমতা আর সহজ ব্যবহার — তিনটি একসাথে সর্বোচ্চ পাওয়া যায় না। TypeScript বেছে নিয়েছে “বেশিরভাগ বাগ ধরো, বাস্তব JavaScript-কে আটকিও না”. তাই এর ফাঁকগুলো জানা থাকা জরুরি — বিশেষ করে সীমানায় নিজে যাচাই করা।

প্র ০৩“টাইপ চলার সময় মুছে যায়” — Python-এর type hint-ও প্রায় একই দর্শন। দুটির মিল-অমিল কী, আর এই দর্শনের ঝুঁকি কোথায়?

Python-এর পথটি আকর্ষণীয়ভাবে সমান্তরাল। Python 3.0 (২০০৮) function annotation-এর syntax যোগ করে (PEP 3107), কিন্তু তার অর্থ ঠিক করে দেয়নি। এরপর Jukka Lehtosalo তাঁর গবেষণা থেকে mypy নামে একটি আলাদা static checker বানান। ২০১৪ সালে Guido van Rossum, Jukka Lehtosalo আর Łukasz Langa PEP 484 লেখেন, যা type hint-এর মানসম্মত অর্থ দেয়; এটি আসে ২০১৫ সালের Python 3.5-এ। মূল নীতি TypeScript-এর মতোই: টাইপ ঐচ্ছিক, যাচাই করে আলাদা টুল (mypy, পরে pyright ইত্যাদি), আর interpreter চলার সময় টাইপ মেনে চলতে বাধ্য করে না। def add(a: int, b: int)-এ string পাঠালেও Python চালিয়ে দেবে।

অমিলও আছে। TypeScript-এ টাইপ সত্যিই মুছে যায় — আউটপুট JavaScript-এ কোনো চিহ্ন থাকে না। Python-এ annotation চলার সময়ও object হিসেবে থাকে (__annotations__), আর প্রোগ্রাম চাইলে পড়তে পারে। এজন্যই Python-এ pydantic বা FastAPI-র মতো লাইব্রেরি type hint পড়েই চলার সময়ের যাচাই করে। JavaScript-এ এটি অসম্ভব, তাই দিক উল্টে গেছে: Zod-এ আগে চলার সময়ের schema লেখা হয়, আর টাইপ সেখান থেকে বের করা হয় (z.infer). আরেকটি পার্থক্য: TypeScript একটি আলাদা ভাষা (.ts ফাইল, compile ধাপ), Python-এর hint ভাষার নিজের অংশ — কোনো build ধাপ নেই।

এই দর্শনের সুবিধা স্পষ্ট: পুরোনো কোড না ভেঙে ধীরে গ্রহণ, runtime-এ কোনো খরচ নেই, আর ভাষার মূল আচরণ অপরিবর্তিত। ঝুঁকিটাও একই জায়গায়: “টাইপ আছে” আর “ডেটা সঠিক” এক জিনিস নয়। প্রোগ্রামের ভেতরের কোড compiler যাচাই করে, কিন্তু সীমানা — নেটওয়ার্ক, ফাইল, ব্যবহারকারীর ইনপুট, ডেটাবেস — সেখানে টাইপ কেবল একটি দাবি। দাবির সাথে বাস্তবের অমিল হলে বাগ আরও বিভ্রান্তিকর হয়, কারণ টাইপ দেখে সবাই নিশ্চিন্ত থাকে।

এ কারণেই দুই জগতেই একই পরামর্শ তৈরি হয়েছে: “parse, don't validate” — সীমানায় একবার ডেটা পরীক্ষা করে টাইপসহ মানে রূপান্তর করো, তারপর ভেতরে সেই টাইপের ওপর নির্ভর করো। TypeScript-এ unknown + schema, Python-এ pydantic model.

মূল উপলব্ধি: মুছে যাওয়া টাইপ হলো প্রোগ্রামারদের মধ্যে আর প্রোগ্রামার ও টুলের মধ্যে একটি চুক্তি — বাইরের জগতের সাথে নয়। বাইরের জগতের জন্য চলার সময়ের যাচাই লাগবেই, ভাষা যা-ই হোক।

প্র ০৪TypeScript compiler নিজেই TypeScript-এ লেখা ছিল। ২০২৫ সালে কেন একে Go ভাষায় নতুন করে লেখার ঘোষণা এল?

TypeScript compiler (tsc) দীর্ঘদিন TypeScript-এই লেখা, আর চলে JavaScript হিসেবে, Node.js-এ। নিজের ভাষায় নিজের compiler লেখাকে বলে self-hosting — এতে দল প্রতিদিন নিজের পণ্য ব্যবহার করে। কিন্তু কোডবেস যত বড় হয়েছে, সমস্যা স্পষ্ট হয়েছে। লাখো লাইনের প্রজেক্টে পুরো টাইপ-যাচাইয়ে অনেক সময় লাগে, editor খুলতে দেরি হয়, মেমরি খরচ বেশি। JavaScript মূলত single-threaded (পর্ব ৩১), আর JIT-নির্ভর — একটি compiler-এর মতো CPU-নির্ভর, দীর্ঘ কাজের জন্য আদর্শ নয়।

২০২৫ সালের মার্চে Anders Hejlsberg ঘোষণা দেন, compiler আর language service-কে Go-তে port করা হচ্ছে (ভেতরের নাম ছিল Project Corsa). ঘোষণায় বলা হয়, বড় প্রজেক্টে টাইপ-যাচাই প্রায় ১০ গুণ দ্রুত হবে — উদাহরণ হিসেবে VS Code-এর নিজের কোডবেস দেখানো হয়েছিল — আর মেমরি ব্যবহারও কমবে। ২০২৫ সালেই পরীক্ষার জন্য preview প্রকাশ করা হয়। পরিকল্পনামতো JavaScript-এ লেখা ধারার শেষ বড় সংস্করণ হয় TypeScript 6.0 (২০২৬-এর শুরুতে), আর native compiler আসে TypeScript 7 নামে (২০২৬-এর মাঝামাঝি) — যাতে উত্তরণ মসৃণ হয়।

প্রশ্ন উঠেছিল: Rust বা C# নয় কেন? দলের ব্যাখ্যা ছিল, এটি “নতুন করে লেখা” নয়, “port” — বিদ্যমান কোডের গঠন আর আচরণ যতটা সম্ভব হুবহু রাখা, যাতে দশকের পর দশকের খুঁটিনাটি আচরণ বদলে না যায়। Go-র কোডের ধরন বিদ্যমান TypeScript কোডের সাথে বেশ মেলে, এতে garbage collection আছে (compiler-এর জটিল, চক্রাকার ডেটা কাঠামোয় সুবিধাজনক), আর shared-memory concurrency দিয়ে একাধিক CPU core একসাথে কাজে লাগানো সহজ।

এই ঘটনা একটি বড় ধারার অংশ। JavaScript টুলিং ক্রমেই native ভাষায় সরছে: esbuild (Go), SWC আর Rolldown (Rust), Biome (Rust). পর্ব ৪৬-এর Vite-ও এসব টুলের ওপর দাঁড়িয়ে। কারণ সহজ — টুল চলে ডেভেলপারের কম্পিউটারে আর CI-তে, দিনে হাজারবার; প্রতি সেকেন্ড বাঁচানো মানে বিশাল সময়। আপনার লেখা TypeScript কোড বা তার আচরণ এতে বদলায় না — বদলায় শুধু যাচাইয়ের গতি।

মূল উপলব্ধি: ভাষা বেছে নেওয়া কাজের ধরনের ওপর নির্ভর করে। যে ভাষা অ্যাপ লেখার জন্য দারুণ, তা বড় compiler-এর জন্য সেরা না-ও হতে পারে। আর বড় প্রকৌশল-সিদ্ধান্তে “নতুন করে লিখি” প্রলোভনের চেয়ে “আচরণ হুবহু রেখে স্থানান্তর” অনেক কম ঝুঁকির।

প্র ০৫JavaScript-এ সরাসরি টাইপ লেখার প্রস্তাব (TC39-এর “type annotations”) কী বলে? এটি গৃহীত হলে কি TypeScript অপ্রয়োজনীয় হয়ে যাবে?

২০২২ সালের মার্চে TC39-এ — যে কমিটি ECMAScript মানদণ্ড ঠিক করে — একটি প্রস্তাব উপস্থাপিত হয়, যা প্রচলিতভাবে “types as comments” নামে পরিচিত। Microsoft-এর Daniel Rosenwasser সহ কয়েকজন এর champion. প্রস্তাবটি Stage 1-এ পৌঁছায়, অর্থাৎ কমিটি মেনে নেয় সমস্যাটি নিয়ে কাজ করার মতো। এরপর বহু বছর ধরে এটি সেই প্রাথমিক পর্যায়েই আছে — গৃহীত হবে কি না, হলে কোন রূপে, তা অনিশ্চিত।

মূল ধারণা চতুর: JavaScript engine টাইপ বুঝবে না, যাচাইও করবে না — শুধু নির্দিষ্ট জায়গার টাইপ-syntax (: number, interface ইত্যাদি) comment-এর মতো উপেক্ষা করবে। যাচাই করবে আলাদা টুল, আজকের মতোই। তাহলে লাভ কী? সরাসরি browser-এ .js ফাইলে টাইপ লিখে চালানো যাবে, কোনো build ধাপ ছাড়াই। ছোট প্রজেক্ট, শেখা আর দ্রুত পরীক্ষায় এটি বড় সুবিধা।

চ্যালেঞ্জও কম নয়। কোন syntax “উপেক্ষাযোগ্য” হবে, তা ঠিক করা কঠিন — TypeScript-এর syntax বিশাল আর প্রতি বছর বাড়ে; মানদণ্ডে একটি নির্দিষ্ট ব্যাকরণ জমে গেলে ভবিষ্যতের নতুন টাইপ-syntax-এর জায়গা থাকতে হবে। enum, namespace বা parameter property-র মতো যেসব জিনিস চলার সময়ের কোড বানায়, সেগুলো এতে আসবে না। Engine-কে বাড়তি syntax parse করতে হবে। আর কেউ কেউ আশঙ্কা করেন, “লেখা আছে কিন্তু যাচাই হয় না” এমন টাইপ নতুনদের বিভ্রান্ত করবে — মনে হবে browser টাইপ যাচাই করছে।

লক্ষ করুন, এই দিকে বাস্তুতন্ত্র আগে থেকেই এগোচ্ছে। Node.js-এর type stripping ঠিক এই আচরণই করে — টাইপ মুছে চালায়, যাচাই করে না। TypeScript 5.8-এর erasableSyntaxOnly কোডকে এই ভবিষ্যতের জন্য প্রস্তুত রাখে। অর্থাৎ প্রস্তাব গৃহীত হোক বা না হোক, “TypeScript = মুছে ফেলা যায় এমন টাইপ-স্তর” ধারণাটি শক্ত হচ্ছে। আর TypeScript অপ্রয়োজনীয় হবে না: যাচাইয়ের কাজটা তো checker-এরই থাকবে — শুধু build ধাপটি হয়তো ঐচ্ছিক হবে।

মূল উপলব্ধি: ভাষার বিবর্তন প্রায়ই চলে এভাবে — বাইরের টুল একটি ধারণা প্রমাণ করে, বাস্তুতন্ত্র তার চারপাশে মানিয়ে নেয়, তারপর মানদণ্ড ধীরে, সাবধানে তার সবচেয়ে স্থিতিশীল অংশ গ্রহণ করে। class, module, async — সবই কোনো না কোনো রূপে এই পথ পার হয়েছে।

অনুশীলন

প্রথমে নিজে উত্তর ভাবুন, তারপর মিলিয়ে নিন। চাইলে typescriptlang.org/play-এ (strict চালু রেখে) চালিয়ে দেখতে পারেন।

  1. Error পড়ে ঠিক করুন: strict mode-এ const btn = document.querySelector("button"); btn.addEventListener("click", save); লিখলে error আসে 'btn' is possibly 'null'. কেন? ! ছাড়া দুটি ঠিক উপায় লিখুন।

    querySelector-এর ফেরত টাইপ HTMLButtonElement | null — পাতায় button না থাকলে সত্যিই null আসে, আর strictNullChecks এটি মনে করিয়ে দেয়। উপায় ১ (narrowing): if (!btn) throw new Error("save button পাওয়া যায়নি"); btn.addEventListener(…) — throw-এর পর compiler জানে btn আর null নয়, আর না পেলে error-ও পরিষ্কার। উপায় ২ (optional chaining, পর্ব ০৮): btn?.addEventListener("click", save) — button না থাকলে চুপচাপ কিছুই হবে না; যেখানে element ঐচ্ছিক, সেখানে এটি ঠিক। btn! error চাপা দেয়, কিন্তু button না থাকলে চলার সময় TypeError দেবে।

  2. any নাকি unknown? প্রতিটি ক্ষেত্রে কোন টাইপ নেবেন, কেন? (ক) localStorage.getItem("cart")-কে JSON.parse করার ফল (খ) catch (err)-এর err (গ) একটি পুরোনো JavaScript লাইব্রেরি, যার টাইপ এখনো লেখা হয়নি আর আজই migration শেষ করতে হবে।

    (ক) unknown — localStorage-এর ডেটা পুরোনো app version বা ব্যবহারকারী নিজে বদলে দিতে পারেন; যাচাই করে তবেই Cart হিসেবে ব্যবহার করুন। (খ) unknown — JavaScript-এ যেকোনো কিছু throw করা যায় (string, number, object), তাই err.message পড়ার আগে if (err instanceof Error). Strict mode-এ (useUnknownInCatchVariables) এটিই ডিফল্ট। (গ) এখানে সাময়িক any বাস্তবসম্মত — কিন্তু এক জায়গায় আটকে রাখুন: একটি ছোট .d.ts ফাইলে declare module "old-lib"; লিখলে পুরো module any হয়; এরপর যে কয়েকটি function আসলে ব্যবহার করেন, শুধু তাদের টাইপ লিখে একটি wrapper বানান। any যেন ছড়িয়ে না পড়ে, সেটিই মূল কথা।

  3. কোনটি compile হবে? interface Contact { name: string; phone: string } ধরে: (ক) const a: Contact = { name: "Mim", phone: "017…", city: "Khulna" }; (খ) const raw = { name: "Mim", phone: "017…", city: "Khulna" }; const b: Contact = raw; (গ) const c: Contact = { name: "Mim" };

    (ক) হবে না — নতুন object literal-এ বাড়তি city, তাই excess property check: Object literal may only specify known properties, and 'city' does not exist in type 'Contact'. (খ) হবে — raw একটি আগে থেকে থাকা variable; structural typing-এ প্রয়োজনীয় সব ঘর আছে, বাড়তি ঘর চলে। (গ) হবে না — phone নেই: Property 'phone' is missing in type '{ name: string; }' but required in type 'Contact'. শিক্ষা: “কম” কখনো চলে না; “বেশি” চলে, শুধু fresh literal ছাড়া।

  4. Discriminated union লিখুন: একটি দোকানে পেমেন্ট তিনভাবে হয় — নগদ (cash, কোনো বাড়তি তথ্য নেই), মোবাইল ব্যাংকিং (mobile, সাথে trxId), কার্ড (card, সাথে কার্ডের শেষ চার অঙ্ক last4). Payment টাইপ আর একটি receiptLine(p: Payment): string লিখুন, যেখানে নতুন পদ্ধতি যোগ হলে compiler মনে করিয়ে দেবে।

    একটি সমাধান:

    type Payment =
      | { method: "cash" }
      | { method: "mobile"; trxId: string }
      | { method: "card"; last4: string };
    
    function receiptLine(p: Payment): string {
      switch (p.method) {
        case "cash":   return "নগদে পরিশোধিত";
        case "mobile": return `মোবাইল ব্যাংকিং, TrxID ${p.trxId}`;
        case "card":   return `কার্ড ****${p.last4}`;
        default:       return assertNever(p);
      }
    }

    method হলো discriminant. প্রতিটি case-এ শুধু সেই পদ্ধতির ঘর পড়া যায় — case "cash"-এ p.trxId লিখলে error. পরে | { method: "voucher"; code: string } যোগ করলে assertNever(p)-এর লাইনে error আসবে, যতক্ষণ না নতুন case লেখেন। last4 string রাখা হয়েছে, number নয় — "0042"-এর সামনের শূন্য যেন না হারায় (পর্ব ০৬-এর নিয়ম)।

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

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

ক্রমএকটি TypeScript ফাইল লেখা থেকে চালানো পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. .ts ফাইলে JavaScript আর type annotation লেখা
  2. Type checker কোড না চালিয়ে টাইপ মিলিয়ে দেখে
  3. error থাকলে লাইন-কলামসহ জানায়, প্রোগ্রামার ঠিক করেন
  4. টাইপ মুছে সাধারণ JavaScript তৈরি হয় (emit বা strip)
  5. Browser বা Node.js টাইপহীন JavaScript চালায়

TypeScript-এ const n: number = JSON.parse('"5"'); লিখে চালানো হলো। চলার সময় typeof n কী দেবে?

  • "number" — টাইপ লেখা আছে, তাই রূপান্তর হয়ে যায়
  • compile error — string-কে number-এ বসানো যায় না
  • "string" — টাইপ মুছে যায়, চলার সময় কোনো যাচাই নেই
  • চলার সময় TypeError

API থেকে আসা ডেটার জন্য any-এর বদলে unknown ব্যবহারের মূল সুবিধা কী?

  • unknown চলার সময় ডেটা নিজে যাচাই করে
  • যাচাই বা narrowing না করে unknown মান ব্যবহার করতে দেয় না
  • unknown দিয়ে কোড দ্রুত চলে
  • unknown-এ any-এর চেয়ে বেশি কাজ বিনা বাধায় করা যায়

interface Point { x: number; y: number } আছে। const p = { x: 1, y: 2, z: 3 }; const q: Point = p; — কী হবে?

  • compile হবে — আকার মেলে, বাড়তি z variable থেকে এলে চলে
  • error — z ঘরটি Point-এ নেই
  • error — p কখনো Point বলে ঘোষণা হয়নি
  • compile হবে, আর চলার সময় z মুছে ফেলা হবে

Discriminated union-এর switch-এর default-এ const x: never = order; লেখার উদ্দেশ্য কী?

  • order-এর মান মুছে ফেলা
  • চলার সময় প্রোগ্রাম থামিয়ে দেওয়া
  • order-কে any-তে রূপান্তর করা
  • নতুন কোনো অবস্থা যোগ হলে আর case না লিখলে compile error পাওয়া

const user = JSON.parse(text) as User; — এই লাইন সম্পর্কে কোনটি সঠিক?

  • as চলার সময় পরীক্ষা করে, User না হলে error দেয়
  • as JSON-কে User object-এ রূপান্তর করে
  • as শুধু compiler-কে বিশ্বাস করতে বলে; ডেটা ভুল হলে পরে কোথাও বাগ হবে
  • এটি সবসময় compile error দেয়

type Product = { id: number; name: string; price: number } থেকে নতুন পণ্য তৈরির জন্য id ছাড়া একটি টাইপ বানাতে কোন utility type সবচেয়ে উপযুক্ত?

  • Partial<Product>
  • Omit<Product, "id">
  • Readonly<Product>
  • ReturnType<Product>

শব্দকোষ

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

মনে রাখুন

  1. TypeScript = JavaScript + টাইপ. Checker চালানোর আগে যাচাই করে, তারপর টাইপ মুছে সাধারণ JavaScript হয় — চলার সময় কোনো টাইপ-যাচাই নেই।
  2. নতুন প্রজেক্টে strict: true. Vite/esbuild/Node শুধু টাইপ মোছে — যাচাইয়ের জন্য editor আর tsc --noEmit.
  3. সীমানায় টাইপ লিখুন (parameter, ডেটার আকার), ভেতরে inference. বাইরের ডেটা unknown, তারপর type guard বা Zod দিয়ে যাচাই।
  4. Structural typing: দরকারি ঘর থাকলেই মেলে, বাড়তি ঘর চলে — শুধু নতুন object literal-এ excess property check. Union + narrowing + discriminated union + never = সব অবস্থা সামলানোর প্রমাণ।
  5. any, as, !, @ts-ignore হলো error চাপা দেওয়ার অস্ত্র — প্রতিটির প্রমাণ চাই। enum-এর বদলে union literal; সত্যিকারের গোপনীয়তায় #private.

পরের পর্বে: টাইপ বলে “price একটি number” — কিন্তু ছাড়ের পর price ঠিক কত হবে, তা বলে না। সেই প্রশ্নের উত্তর দেয় টেস্ট। Vitest দিয়ে unit test, আর Playwright দিয়ে সত্যিকারের browser-এ পুরো অ্যাপ চালিয়ে পরীক্ষা।

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

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