আপনি এমন একটি টাইপ লেখেন যা নেস্টেড অবজেক্টগুলোর মধ্য দিয়ে ঘুরে বেড়ায় এবং অটো-কমপ্লিটের জন্য ডট-সেপারেটেড পাথ (dot-separated paths) তৈরি করে। একটি ছোট টেস্ট অবজেক্টের ক্ষেত্রে এটি চমৎকারভাবে কাজ করে। কিন্তু যখন আপনি এটি একটি রিয়েল API পেলোডের (payload) ওপর প্রয়োগ করেন, তখন এডিটর ফ্রিজ হয়ে যায়। অবশেষে, TypeScript আপনাকে error TS2589 প্রদান করে: Type instantiation is excessively deep and possibly infinite.

এই মেসেজটির মানে এই নয় যে আপনার কোডে প্রথাগত অর্থে কোনো ইনফিনিট লুপ (infinite loop) আছে। এর মানে হলো কম্পাইলার হাল ছেড়ে দিয়েছে। আপনি যে টাইপটি গণনা করতে বলেছিলেন সেটি হয় প্রকৃতপক্ষে আনবাউন্ডেড (unbounded), অথবা ফিনিট (finite) কিন্তু এত বড় যে এটি মূল্যায়ন করতে গেলে TypeScript-এর অভ্যন্তরীণ সীমা অতিক্রম হয়ে যাবে। যখন এমনটি ঘটে, তখন আপনার IDE হ্যাং হওয়ার আগেই কম্পাইলার কাজ থামিয়ে দেয়।

কখন TS2589 দেখা দেয়

রিকার্সিভ টাইপ (Recursive types) হলো এর সবচেয়ে সাধারণ কারণ। TypeScript টাইপগুলোকে অত্যন্ত দ্রুত (eagerly) মূল্যায়ন করে, এবং যদি একটি ইউটিলিটি টাইপ বারবার নিজেকে কল করতে থাকে—বিশেষ করে কন্ডিশনাল লজিকের মাধ্যমে—তবে কম্পিউটেশন স্ট্যাক (computation stack) দ্রুত বৃদ্ধি পায়। আপনি সাধারণত কয়েকটি নির্দিষ্ট পরিস্থিতিতে এই সমস্যার সম্মুখীন হবেন:

  • রিকার্সিভ কন্ডিশনাল টাইপ (Recursive conditional types) যা একটি বেস কেস (base case) না পাওয়া পর্যন্ত বারবার একটি টাপল (tuple), অবজেক্ট বা স্ট্রিং টেমপ্লেটকে ডিস্ট্রাকচার করে
  • গভীরভাবে নেস্টেড অবজেক্ট পাথ জেনারেটর (Deeply nested object path generators), যা { user: { address: { street: string } } }-এর মতো স্ট্রাকচারকে "user" | "user.address" | "user.address.street"-এর মতো স্ট্রিং লিটারেল ইউনিয়নের (unions of string literals) রূপান্তর করে
  • টেমপ্লেট লিটারেল টাইপ (Template literal types) যা স্ট্রিংকে ক্যারেক্টার বাই ক্যারেক্টার বা টোকেন বাই টোকেন পার্স করে
  • ম্যাপড টাইপ (Mapped types) যা ডজন ডজন কী (key) এবং একাধিক লেভেল বিশিষ্ট অবজেক্টের ওপর ইটারেট করে
  • কন্ডিশনাল টাইপ (Conditional types) যা বড় ইউনিয়নের ওপর ডিস্ট্রিবিউট (distribute) হয় এবং প্রতিটি মেম্বারের ওপর কাজের চাপ নীরবে বহুগুণ বাড়িয়ে দেয়

নেস্টেড পাথের উদাহরণটি বিশেষভাবে প্রলুব্ধকর। ফর্ম লাইব্রেরি এবং স্টেট-ম্যানেজমেন্ট টুলগুলো টাইপড পাথ (typed paths) অফার করতে পছন্দ করে যাতে আপনি ফিল্ডের নামের জন্য অটো-কমপ্লিট পান। একটি অগভীর (shallow) অবজেক্টের ক্ষেত্রে, প্রতিটি বৈধ ডট-পাথকে স্ট্রিং ইউনিয়ন হিসেবে তৈরি করা খুব সহজ। কিন্তু একটি গভীর বা প্রশস্ত (wide) অবজেক্টের ক্ষেত্রে, সেই ইউনিয়নটি বিশাল আকার ধারণ করে। TypeScript-কে প্রতিটি পারমুটেশন (permutation) একসাথে ওয়ার্কিং মেমোরিতে রাখতে হয়। একটি নির্দিষ্ট গভীরতায়, কম্পাইলার বুঝতে পারে যে কাজের গতি তার বাজেটের চেয়ে বেশি হয়ে যাচ্ছে এবং জরুরি ব্রেক চেপে ধরে।

সমাধান ১: একটি নির্দিষ্ট ডেপথ লিমিট (Hard Depth Limit) যোগ করুন

TS2589 সমাধানের সবচেয়ে সরাসরি উপায় হলো আপনার টাইপটি যে অনন্তকাল রিকার্স করতে পারে সেই ধারণাটি ত্যাগ করা। একটি ডেপথ কাউন্টার (depth counter) যুক্ত করুন যা সার্কিট ব্রেকার হিসেবে কাজ করবে।

বাস্তবে, এর মানে হলো একটি নিউমেরিক জেনেরিক প্যারামিটার যোগ করা—যা প্রায়শই একটি টাপল (tuple) হিসেবে প্রকাশ করা হয় যার দৈর্ঘ্য প্রতিবার রিকার্সনের সাথে কমতে থাকে। যখন কাউন্টার শূন্যে পৌঁছায়, তখন টাইপটি আরও গভীরে না গিয়ে string-এর মতো একটি ব্রড ফলব্যাক (broad fallback) রিটার্ন করে। ব্যবহারকারীরা প্রথম চার বা পাঁচটি লেভেলের জন্য নির্ভুল অটো-কমপ্লিট পাবেন, যা বাস্তব জগতের বেশিরভাগ অবজেক্টের জন্য যথেষ্ট। এর বাইরে, কম্পাইলার কেবল টাইপটিকে ওয়াইডেন (widen) করে এবং এগিয়ে যায়।

এই পদ্ধতিটি আপনার ইউটিলিটি টাইপকে কোনোভাবে কম সঠিক করে তোলে না। এটি এটিকে সীমাবদ্ধ (bounded) করে। এমন একটি টাইপ সিস্টেম যা কম্পাইলারকে ক্র্যাশ করায়, তা একটি বুদ্ধিদীপ্ত গভীরতার পর মেনে নেওয়া টাইপ সিস্টেমের চেয়ে বেশি কার্যকর নয়।

সমাধান ২: একবারে একটি পাথ যাচাই করুন

যদি আগে থেকেই প্রতিটি সম্ভাব্য পাথ তৈরি করা অনেক ব্যয়বহুল হয়, তবে চুক্তিতে পরিবর্তন আনুন। সমস্ত বৈধ স্ট্রিংয়ের একটি বিশাল ইউনিয়ন তৈরি করার পরিবর্তে, এমন একটি টাইপ লিখুন যা যাচাই করে যে একটি নির্দিষ্ট স্ট্রিং একটি বৈধ পাথ কি না।

প্রতিটি ইংরেজি শব্দের একটি ডিকশনারি তৈরি করা এবং একটি শব্দ সঠিকভাবে বানান করা হয়েছে কি না তা যাচাই করার মধ্যে যে পার্থক্য, সেটি চিন্তা করুন। প্রথমটি একটি বিশাল ডেটা স্ট্রাকচার; দ্বিতীয়টি একটি হালকা স্ক্যান। TypeScript-এর ভাষায়, "user.address.street" | "user.settings.theme" | ... প্রদানকারী একটি Paths<T> ইউটিলিটি এক্সপোর্ট করার পরিবর্তে, আপনি IsValidPath<T, "user.address.street">-এর মতো কিছু এক্সপোর্ট করুন। কম্পাইলার কেবল সেই পাথটিই মূল্যায়ন করবে যা আপনি আসলে পাস করছেন।

এই পরিবর্তনটি আপনার API ডিজাইনের ধরন বদলে দেয়। আপনার ফাংশন সিগনেচারগুলো একটি স্ট্রিং গ্রহণ করতে পারে এবং তারপর অবজেক্ট শেপের (object shape) বিপরীতে এটি যাচাই করতে একটি জেনেরিক কনস্ট্রেইন্ট (generic constraint) ব্যবহার করতে পারে। ডেভেলপার যদি ভুল পাথ টাইপ করেন তবে IDE তবুও অভিযোগ করবে, কিন্তু টাইপ-চেকিংয়ের সময় কম্পাইলারকে কখনোই বৈধ পাথের সম্পূর্ণ সেট তৈরি করতে হবে না। বড় অবজেক্টের ক্ষেত্রে পারফরম্যান্সের পার্থক্যটি নাটকীয়।

দ্রুত কৌশল যা আপনাকে এগিয়ে রাখতে সাহায্য করবে

এই দুটি কাঠামোগত সমাধানের বাইরেও, কয়েকটি ছোট অভ্যাস রিকার্সিভ টাইপগুলোকে সীমা অতিক্রম করা থেকে দূরে রাখতে পারে:

  • টাইপ প্যারামিটারগুলোকে টাপল (tuple)-এ মুড়িয়ে দিন যাতে ডিস্ট্রিবিউশন (distribution) বন্ধ করা যায়। একটি কন্ডিশনাল-এ সরাসরি টাইপ প্যারামিটার ব্যবহার করলে, যেমন T extends Foo ? Bar : Baz, T যদি একটি ইউনিয়ন (union) হয়, তবে এটি প্রতিটি মেম্বারের জন্য চেকটি ডিস্ট্রিবিউট করে। যদি সেই ইউনিয়নে পঞ্চাশটি মেম্বার থাকে, তবে TypeScript পঞ্চাশটি আলাদা ইন্সট্যান্সিয়েশন (instantiation) সম্পন্ন করে। [T] extends [Foo] ? Bar : Baz লিখলে কন্ডিশনালটি পুরো ইউনিয়নের বিপরীতে একবারই মূল্যায়ন করা হয়। যখন আপনার টাইপটিকে প্রতিটি ইউনিয়ন মেম্বারের ওপর আলাদাভাবে ম্যাপ করার প্রয়োজন নেই, তখনই এটি ব্যবহার করুন।

  • ডিবাগিং করার সময় আপনার ইনপুট ছোট করে ফেলুন। যখন TS2589 ত্রুটিটি দেখা দেয়, তখন আপনার প্রোডাকশন অবজেক্ট টাইপটিকে দুটি প্রপার্টি এবং এক লেভেল নেস্টিং বিশিষ্ট একটি ছোট স্টাব (stub)-এর মাধ্যমে পরিবর্তন করুন। যদি ত্রুটিটি চলে যায়, তবে আপনি নিশ্চিত হতে পারবেন যে সমস্যাটি ডেপথ (depth) বা কার্ডিনালিটি (cardinality)-তে, সিনট্যাক্স ভুলের কারণে নয়। এটি আপনাকে এমন লজিক পুনরায় লেখার ঝামেলা থেকে বাঁচায় যা গঠনগতভাবে আসলে ঠিক ছিল।

  • পাবলিক-ফেসিং API টাইপগুলোকে কিছুটা শিথিল করুন। অভ্যন্তরীণভাবে আপনার সূক্ষ্ম নির্ভুলতার প্রয়োজন হতে পারে। কিন্তু বাহ্যিকভাবে, নিখুঁত হওয়ার চেষ্টা অনেক সময় উপকারের চেয়ে ক্ষতি বেশি করতে পারে। যদি একটি সামান্য বিস্তৃত অটো-কমপ্লিট (autocomplete) টাইপ এডিটরে দুই সেকেন্ডের ল্যাগ প্রতিরোধ করতে পারে, তবে সেই বিনিময়টি সাধারণত সার্থক। আপনি টেস্টিংয়ের সময় ভুল পাথগুলো ধরার জন্য এই শিথিল টাইপটির সাথে একটি রানটাইম ভ্যালিডেটর (runtime validator) ব্যবহার করতে পারেন।

কেন TypeScript এই সীমাবদ্ধতা আরোপ করে

TypeScript 'halting problem' সমাধান করতে পারে না। আপনার রিকার্সিভ টাইপটি শেষ পর্যন্ত শেষ হবে নাকি অনন্তকাল চলতে থাকবে, তা এটি জানে না। কম্পাইলারের ভেতরে একটি ইনফিনিট লুপের ঝুঁকি নেওয়ার পরিবর্তে, এটি একটি রক্ষণশীল কাট-অফ (cutoff) আরোপ করে। মাঝে মাঝে সেই কাট-অফ এমন একটি টাইপকে আটকে দেয় যা পর্যাপ্ত সময় দিলে শেষ হতে পারত। TS2589 হলো কম্পাইলারের একটি স্বীকারোক্তি যে, ভুল হওয়ার চেয়ে নিরাপদ থাকাই তার কাছে শ্রেয়।

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

আসল শিক্ষা

TS2589 মানে এই নয় যে আপনি একজন খারাপ টাইপ-সিস্টেম প্রোগ্রামার। এটি একটি সংকেত যে আপনার টাইপটি একসাথে অনেক বেশি কাজ করছে। আপনার রিকার্সন সীমিত রাখুন, লেজি ভ্যালিডেশন (lazy validation) করুন এবং অপ্রয়োজনীয় ডিস্ট্রিবিউশন থেকে রক্ষা করুন। অ্যাডভান্সড টাইপগুলোর লক্ষ্য কম্পাইল টাইমে প্রতিটি সম্ভাব্য সত্য প্রমাণ করা নয়; বরং আপনার টিমকে দ্রুত এবং নির্ভরযোগ্য টুলিং প্রদান করা। যে টাইপটি মিলিসেকেন্ডের মধ্যে কম্পাইল হয় এবং পঁচানব্বই শতাংশ কেস কভার করে, সেটি তাত্ত্বিকভাবে নিখুঁত কিন্তু ল্যাঙ্গুয়েজ সার্ভার ক্র্যাশ করে এমন টাইপের চেয়ে অনেক বেশি মূল্যবান।