നിങ്ങൾ ഒരു ടൈപ്പ് എഴുതുന്നു, അത് നെസ്റ്റഡ് ഒബ്ജക്റ്റുകളിലൂടെ സഞ്ചരിച്ച് ഓട്ടോകംപ്ലീറ്റിനായി ഡോട്ട് ഉപയോഗിച്ച് വേർതിരിച്ച പാത്തുകൾ (dot-separated paths) നിർമ്മിക്കുന്നു. ഒരു ചെറിയ ടെസ്റ്റ് ഒബ്ജക്റ്റിൽ ഇത് മികച്ച രീതിയിൽ പ്രവർത്തിക്കുന്നു. എന്നാൽ നിങ്ങൾ ഇത് ഒരു യഥാർത്ഥ API പേലോഡിലേക്ക് (payload) ഉപയോഗിക്കുമ്പോൾ എഡിറ്റർ ഫ്രീസ് ആകുന്നു. ഒടുവിൽ, TypeScript TS2589: Type instantiation is excessively deep and possibly infinite. എന്ന എറർ കാണിക്കുന്നു.
ഈ സന്ദേശം നിങ്ങളുടെ കോഡിൽ ഒരു ഇൻഫിനിറ്റ് ലൂപ്പ് (infinite loop) ഉണ്ടെന്ന് അർത്ഥമാക്കുന്നില്ല. ഇതിനർത്ഥം കംപൈലർ ശ്രമം ഉപേക്ഷിച്ചു എന്നാണ്. നിങ്ങൾ കണക്കുകൂട്ടാൻ ആവശ്യപ്പെട്ട ടൈപ്പ് ഒന്നുകിൽ പരിധിയില്ലാത്തതായിരുന്നു, അല്ലെങ്കിൽ പരിമിതമാണെങ്കിലും വളരെ വലുതായിരുന്നു, അത് വിലയിരുത്തുന്നത് TypeScript-ന്റെ ആന്തരിക പരിധികളെ മറികടക്കും. ഇങ്ങനെയുണ്ടാകുമ്പോൾ, നിങ്ങളുടെ IDE ഹാങ്ങ് ആകുന്നതിന് മുമ്പ് കംപൈലർ പ്രവർത്തനം നിർത്തിവെക്കുന്നു.
TS2589 എപ്പോൾ സംഭവിക്കുന്നു
റിക്കേഴ്സീവ് ടൈപ്പുകളാണ് (Recursive types) ഇതിന് പ്രധാന കാരണം. TypeScript ടൈപ്പുകളെ വളരെ വേഗത്തിൽ (eagerly) വിലയിരുത്തുന്നു, കൂടാതെ ഒരു യൂട്ടിലിറ്റി ടൈപ്പ് സ്വയം ആവർത്തിച്ച് വിളിച്ചുകൊണ്ടിരിക്കുകയാണെങ്കിൽ—പ്രത്യേകിച്ച് കണ്ടിഷണൽ ലോജിക് (conditional logic) വഴി—കമ്പ്യൂട്ടേഷൻ സ്റ്റാക്ക് വേഗത്തിൽ വളരും. സാധാരണയായി താഴെ പറയുന്ന സാഹചര്യങ്ങളിലാണ് നിങ്ങൾ ഈ പ്രശ്നം നേരിടുന്നത്:
- ഒരു ട്യൂപ്പിനെ (tuple), ഒബ്ജക്റ്റിനെ അല്ലെങ്കിൽ സ്ട്രിംഗ് ടെംപ്ലേറ്റുകളെ ഒരു ബേസ് കേസ് (base case) ലഭിക്കുന്നത് വരെ ആവർത്തിച്ച് ഡിസ്ട്രക്ചർ ചെയ്യുന്ന റിക്കേഴ്സീവ് കണ്ടിഷണൽ ടൈപ്പുകൾ (Recursive conditional types).
{ user: { address: { street: string } } }പോലുള്ള ഘടനകളെ"user" | "user.address" | "user.address.street"എന്നിങ്ങനെയുള്ള സ്ട്രിംഗ് ലിറ്ററൽ യൂണിയനുകളാക്കി മാറ്റുന്ന ഡീപ്ലി നെസ്റ്റഡ് ഒബ്ജക്റ്റ് പാത്ത് ജനറേറ്ററുകൾ (Deeply nested object path generators).- സ്ട്രിംഗുകളെ ഓരോ ക്യാരക്ടർ അല്ലെങ്കിൽ ടോക്കൺ ആയി വിശകലനം ചെയ്യുന്ന ടെംപ്ലേറ്റ് ലിറ്ററൽ ടൈപ്പുകൾ (Template literal types).
- ഡസൻ കണക്കിന് കീകളും (keys) ഒന്നിലധികം ലെവലുകളും ഉള്ള ഒബ്ജക്റ്റുകളിൽ ഉപയോഗിക്കുന്ന മാപ്പ്ഡ് ടൈപ്പുകൾ (Mapped types).
- വലിയ യൂണിയനുകളിൽ വിതരണം ചെയ്യപ്പെടുന്ന കണ്ടിഷണൽ ടൈപ്പുകൾ (Conditional types), ഇത് വർക്ക് ലോഡ് ഓരോ മെമ്പറിലേക്കും നിശബ്ദമായി വർദ്ധിപ്പിക്കുന്നു.
നെസ്റ്റഡ് പാത്ത് ഉദാഹരണം പ്രത്യേകിച്ച് ആകർഷകമാണ്. ഫോം ലൈബ്രറികളും സ്റ്റേറ്റ് മാനേജ്മെന്റ് ടൂളുകളും ഫീൽഡ് പേരുകൾക്കായി ഓട്ടോകംപ്ലീറ്റ് ലഭിക്കുന്നതിനായി ടൈപ്പ് ചെയ്ത പാത്തുകൾ നൽകാൻ ഇഷ്ടപ്പെടുന്നു. ഒരു ഷാലോ ഒബ്ജക്റ്റിൽ (shallow object), എല്ലാ സാധ്യമായ ഡോട്ട്-പാത്തുകളും ഒരു സ്ട്രിംഗ് യൂണിയനായി നിർമ്മിക്കുന്നത് ലളിതമാണ്. എന്നാൽ ഒരു ഡീപ് അല്ലെങ്കിൽ വൈഡ് ഒബ്ജക്റ്റിൽ, ആ യൂണിയൻ പെട്ടെന്ന് വർദ്ധിക്കുന്നു. ഓരോ പെർമുട്ടേഷനും (permutation) ഒരേസമയം മെമ്മറിയിൽ സൂക്ഷിക്കേണ്ടതുണ്ട്. ഒരു നിശ്ചിത ഡെപ്ത്തിൽ എത്തുമ്പോൾ, കംപൈലർ തന്റെ പരിധി ലംഘിക്കുന്നുണ്ടെന്ന് മനസ്സിലാക്കുകയും പ്രവർത്തനം നിർത്തിവെക്കുകയും ചെയ്യുന്നു.
പരിഹാരം ഒന്ന്: ഒരു കൃത്യമായ ഡെപ്ത്ത് ലിമിറ്റ് (Depth Limit) നൽകുക
TS2589 പരിഹരിക്കാനുള്ള ഏറ്റവും നേരിട്ടുള്ള മാർഗ്ഗം നിങ്ങളുടെ ടൈപ്പിന് എന്നെന്നേക്കുമായി റിക്കേഴ്സ് ചെയ്യാൻ കഴിയുമെന്ന് കരുതുന്നത് ഒഴിവാക്കുക എന്നതാണ്. ഒരു സർക്യൂട്ട് ബ്രേക്കർ (circuit breaker) ആയി പ്രവർത്തിക്കുന്ന ഒരു ഡെപ്ത്ത് കൗണ്ടർ (depth counter) കൊണ്ടുവരിക.
പ്രായോഗികമായി പറഞ്ഞാൽ, ടൈപ്പ് റിക്കേഴ്സ് ചെയ്യുമ്പോഴെല്ലാം കുറഞ്ഞുവരുന്ന ഒരു ന്യൂമെറിക് ജനറിക് പാരാമീറ്റർ (numeric generic parameter)—പലപ്പോഴും അതിന്റെ നീളം കുറഞ്ഞുവരുന്ന ഒരു ട്യൂപ്പ് ആയി ഇത് സൂചിപ്പിക്കുന്നു—ചേർക്കുക എന്നതാണ് ഇതിനർത്ഥം. കൗണ്ടർ പൂജ്യത്തിൽ എത്തുമ്പോൾ, കൂടുതൽ ആഴത്തിൽ തിരയുന്നതിന് പകരം string പോലുള്ള ഒരു ബ്രോഡ് ഫാൽബാക്ക് (broad fallback) ടൈപ്പ് തിരികെ നൽകുന്നു. ഉപയോക്താക്കൾക്ക് ആദ്യത്തെ നാലോ അഞ്ചോ ലെവലുകൾക്കായി കൃത്യമായ ഓട്ടോകംപ്ലീറ്റ് ലഭിക്കും, ഇത് ഭൂരിഭാഗം യഥാർത്ഥ ഒബ്ജക്റ്റുകളെയും ഉൾക്കൊള്ളുന്നു. അതിനുശേഷം, കംപൈലർ ടൈപ്പിനെ ലളിതമാക്കുകയും മുന്നോട്ട് പോവുകയും ചെയ്യുന്നു.
ഈ സമീപനം നിങ്ങളുടെ യൂട്ടിലിറ്റി ടൈപ്പിന്റെ കൃത്യതയെ കാര്യമായി ബാധിക്കില്ല. പകരം അത് പരിധി നിശ്ചയിച്ച ഒന്നായി മാറുന്നു. കംപൈലറിനെ ക്രാഷ് ചെയ്യുന്ന ഒരു ടൈപ്പ് സിസ്റ്റത്തേക്കാൾ പ്രയോജനപ്രദമാണ്, ഒരു നിശ്ചിത ഡെപ്ത്തിന് ശേഷം വിട്ടുവീഴ്ച ചെയ്യുന്ന ടൈപ്പ് സിസ്റ്റം.
പരിഹാരം രണ്ട്: ഓരോ പാത്തും പ്രത്യേകം പരിശോധിക്കുക
എല്ലാ സാധ്യമായ പാത്തുകളും മുൻകൂട്ടി നിർമ്മിക്കുന്നത് പ്രയാസകരമാണെങ്കിൽ, ആ രീതി മാറ്റുക. എല്ലാ സ്ട്രിംഗുകളുടെയും ഒരു വലിയ യൂണിയൻ ഉണ്ടാക്കുന്നതിന് പകരം, ഒരു പ്രത്യേക സ്ട്രിംഗ് സാധുവായ പാത്ത് ആണോ എന്ന് പരിശോധിക്കുന്ന ഒരു ടൈപ്പ് എഴുതുക.
എല്ലാ ഇംഗ്ലീഷ് വാക്കുകളും ഉൾക്കൊള്ളുന്ന ഒരു ഡിക്ഷണറി ഉണ്ടാക്കുന്നതും, ഒരു വാക്ക് ശരിയാണോ എന്ന് പരിശോധിക്കുന്നതും തമ്മിലുള്ള വ്യത്യാസം ചിന്തിക്കുക. ആദ്യത്തേത് വലിയൊരു ഡാറ്റാ സ്ട്രക്ചറാണ്; രണ്ടാമത്തേത് ലളിതമായ ഒരു പരിശോധന മാത്രമാണ്. TypeScript പദങ്ങളിൽ പറഞ്ഞാൽ, "user.address.street" | "user.settings.theme" | ... നൽകുന്ന ഒരു Paths<T> യൂട്ടിലിറ്റിക്ക് പകരം, IsValidPath<T, "user.address.street"> പോലുള്ള ഒന്ന് ഉപയോഗിക്കുക. നിങ്ങൾ നൽകുന്ന പാത്ത് മാത്രമേ കംപൈലർ വിലയിരുത്തുകയുള്ളൂ.
ഈ മാറ്റം നിങ്ങളുടെ API ഡിസൈൻ രീതിയെ മാറ്റുന്നു. നിങ്ങളുടെ ഫംഗ്ഷൻ സിഗ്നേച്ചറുകൾ ഒരു സ്ട്രിംഗ് സ്വീകരിക്കുകയും ഒബ്ജക്റ്റ് ഷേപ്പുമായി (object shape) അത് ഒത്തുപോകുന്നുണ്ടോ എന്ന് പരിശോധിക്കാൻ ഒരു ജനറിക് കൺസ്ട്രയിന്റ് (generic constraint) ഉപയോഗിക്കുകയും ചെയ്തേക്കാം. ഡെവലപ്പർ തെറ്റായ ഒരു പാത്ത് ടൈപ്പ് ചെയ്താൽ IDE എറർ കാണിക്കും, പക്ഷേ ടൈപ്പ് ചെക്കിംഗ് സമയത്ത് കംപൈലർ എല്ലാ സാധ്യമായ പാത്തുകളും നിർമ്മിക്കേണ്ടി വരില്ല. വലിയ ഒബ്ജക്റ്റുകളിൽ, ഈ മാറ്റം പെർഫോമൻസിൽ വലിയ വ്യത്യാസം ഉണ്ടാക്കും.
മുന്നോട്ട് പോകാൻ സഹായിക്കുന്ന ചില എളുപ്പവഴികൾ
ഈ രണ്ട് ഘടനാപരമായ പരിഹാരങ്ങൾ കൂടാതെ, റിക്കേഴ്സീവ് ടൈപ്പുകൾ പരിധി ലംഘിക്കാതിരിക്കാൻ ചില ചെറിയ ശീലങ്ങൾ സഹായിക്കും:
ഡിസ്ട്രിബ്യൂഷൻ തടയാൻ ടൈപ്പ് പാരാമീറ്ററുകളെ ട്യൂപ്പിളുകളിൽ (tuples) ഉൾപ്പെടുത്തുക.
T extends Foo ? Bar : Bazപോലുള്ള ഒരു കണ്ടീഷണലിലെ നേരിട്ടുള്ള (naked) ടൈപ്പ് പാരാമീറ്റർ,Tഒരു യൂണിയൻ (union) ആണെങ്കിൽ പരിശോധന ഓരോ അംഗത്തിലേക്കും വിതരണം ചെയ്യുന്നു. ആ യൂണിയനിൽ അമ്പത് അംഗങ്ങൾ ഉണ്ടെങ്കിൽ, TypeScript അമ്പത് തവണ ഇൻസ്റ്റൻഷ്യേഷനുകൾ നടത്തുന്നു.[T] extends [Foo] ? Bar : Bazഎന്ന് എഴുതുന്നത് മുഴുവൻ യൂണിയനെതിരെയുമുള്ള കണ്ടീഷണലിനെ ഒറ്റത്തവണ മാത്രം വിലയിരുത്തുന്നു. ഓരോ യൂണിയൻ അംഗത്തിനും പ്രത്യേകം മാപ്പ് ചെയ്യേണ്ട ആവശ്യമില്ലാത്തപ്പോഴെല്ലാം ഇത് ഉപയോഗിക്കുക.ഡീബഗ്ഗിംഗ് ചെയ്യുമ്പോൾ ഇൻപുട്ടുകൾ കുറയ്ക്കുക. TS2589 തെറ്റായി കാണിക്കുമ്പോൾ, നിങ്ങളുടെ പ്രൊഡക്ഷൻ ഒബ്ജക്റ്റ് ടൈപ്പിന് പകരം രണ്ട് പ്രോപ്പർട്ടികളും ഒരു ലെവൽ നെസ്റ്റിംഗുമുള്ള ചെറിയൊരു സ്റ്റബ് (stub) ഉപയോഗിക്കുക. എറർ മാറുന്നുണ്ടെങ്കിൽ, പ്രശ്നം സിന്റാക്സ് തെറ്റല്ലെന്നും ഡെപ്ത്ത് (depth) അല്ലെങ്കിൽ കാർഡിനാലിറ്റി (cardinality) ആണെന്നും നിങ്ങൾക്ക് ഉറപ്പിക്കാം. ഇത് ഘടനപരമായി ശരിയായ ലോജിക് വീണ്ടും എഴുതുന്നതിൽ നിന്ന് നിങ്ങളെ രക്ഷിക്കുന്നു.
പബ്ലിക്-ഫേസിംഗ് API ടൈപ്പുകൾ ലളിതമാക്കുക. ആന്തരികമായി (Internally), നിങ്ങൾക്ക് കൃത്യത ആവശ്യമായി വന്നേക്കാം. എന്നാൽ പുറമെ, അമിതമായ കൃത്യത ചിലപ്പോൾ ഗുണത്തേക്കാൾ കൂടുതൽ നഷ്ടമുണ്ടാക്കാം. എഡിറ്ററിലെ രണ്ട് സെക്കൻഡ് ലാഗ് ഒഴിവാക്കാൻ അല്പം വിശാലമായ ഒരു autocomplete ടൈപ്പ് സഹായിക്കുന്നുണ്ടെങ്കിൽ, ആ മാറ്റം സാധാരണയായി ലാഭകരമാണ്. ടെസ്റ്റിംഗിൽ തെറ്റായ പാത്തുകൾ കണ്ടെത്താൻ ഈ ലൂസ് ടൈപ്പിനൊപ്പം ഒരു runtime validator കൂടി ഉപയോഗിക്കാം.
എന്തുകൊണ്ടാണ് TypeScript ഈ പരിധി നിശ്ചയിക്കുന്നത്
TypeScript-ന് 'halting problem' പരിഹരിക്കാൻ കഴിയില്ല. നിങ്ങളുടെ റിക്കഴ്സീവ് ടൈപ്പ് (recursive type) എപ്പോഴെങ്കിലും അവസാനിക്കുമോ അതോ അനന്തമായി തുടരുമോ എന്ന് അതിന് അറിയില്ല. കംപൈലറിനുള്ളിൽ ഒരു ഇൻഫിനിറ്റ് ലൂപ്പിന് (infinite loop) സാധ്യത വരുത്തുന്നതിന് പകരം, അത് ഒരു പരിധി നിശ്ചയിക്കുന്നു. ചിലപ്പോൾ മതിയായ സമയം നൽകിയാൽ പൂർത്തിയാകേണ്ട ഒരു ടൈപ്പിനെ ഈ പരിധി തടഞ്ഞേക്കാം. സുരക്ഷിതമായിരിക്കാനാണ് കംപൈലർ ആഗ്രഹിക്കുന്നതെന്ന് TS2589 സൂചിപ്പിക്കുന്നു.
ആ പരിധി മാനിക്കുന്നത് പ്രൊഡക്ഷൻ-ഗ്രേഡ് ടൈപ്പുകൾ എഴുതുന്നതിന്റെ ഭാഗമാണ്. ഒരു ടൈപ്പ് ഡെഫനിഷൻ എന്നത് കംപൈലറിൽ പ്രവർത്തിക്കുന്ന കോഡാണ്, കൂടാതെ സങ്കീർണ്ണമായ കോഡുകൾ യഥാർത്ഥ പ്രത്യാഘാതങ്ങൾ ഉണ്ടാക്കും. സ്ലോ ആയ റൺടൈം കോഡ് യൂസർ എക്സ്പീരിയൻസിനെ ബാധിക്കുന്നത് പോലെ തന്നെ, സ്ലോ ആയ autocomplete ഡെവലപ്പർമാരുടെ വേഗതയെയും ബാധിക്കുന്നു.
യഥാർത്ഥ പാഠം
TS2589 എന്നത് നിങ്ങൾ ഒരു മോശം ടൈപ്പ്-സിസ്റ്റം പ്രോഗ്രാമർ ആണെന്നതിന്റെ സൂചനയല്ല. നിങ്ങളുടെ ടൈപ്പ് ഒരേസമയം ഒരുപാട് ജോലികൾ ചെയ്യുന്നു എന്നതിന്റെ സൂചനയാണ് അത്. റിക്കർഷൻ പരിമിതപ്പെടുത്തുക, ലേസി ആയി വാലിഡേറ്റ് ചെയ്യുക, അനാവശ്യമായ ഡിസ്ട്രിബ്യൂഷൻ ഒഴിവാക്കുക. അഡ്വാൻസ്ഡ് ടൈപ്പുകളുടെ ലക്ഷ്യം കംപൈൽ ടൈമിൽ എല്ലാ സത്യങ്ങളും തെളിയിക്കുക എന്നതല്ല; മറിച്ച് നിങ്ങളുടെ ടീമിന് വേഗതയേറിയതും വിശ്വസനീയവുമായ ടൂളിംഗ് നൽകുക എന്നതാണ്. മില്ലിസെക്കൻഡുകൾക്കുള്ളിൽ കംപൈൽ ചെയ്യുകയും തൊണ്ണൂറ്റഞ്ച് ശതമാനം കേസുകളും കവർ ചെയ്യുകയും ചെയ്യുന്ന ഒരു ടൈപ്പ്, തിയററ്റിക്കലായി പെർഫെക്റ്റ് ആണെങ്കിലും ലാംഗ്വേജ് സെർവർ ക്രാഷ് ചെയ്യുന്നതിനേക്കാൾ എത്രയോ മൂല്യമുള്ളതാണ്.
