07
مهتوی typescript ، یه تایپ بازگشتی یه تایپه که تو تعریف خودش، به خودش ارجاع میده. مثلاً فرض کن میخوای یه ساختار دادهای مثل درخت دودویی یا لیست پیوندی رو مدل کنی. این ساختارها معمولاً به صورت بازگشتی تعریف میشن، چون هر گره میتونه به گرههای دیگه از همون جنس اشاره کنه.
مثلاً یه خط کد رو ببین:
اینجا firstName و ‘Maynard’ توی دنیای مقدارها هستن، ولی string مال دنیای تایپهاست.
یا مثلاً عملگر typeof رو داریم که تو هر دو دنیا کار میکنه. تو دنیای مقدارها اینجوریه:
ولی تو دنیای تایپها، برای استخراج تایپ یه تابع مثل increment استفاده میشه و بعد با یه ابزار به اسم ReturnType کار میکنه:
یکی از چیزای باحال دنیای تایپها، تایپهای بازگشتی هستن. اینا تایپهاییان که به خودشون اشاره میکنن، یه چیزی تو مایههای تابعهای بازگشتی که شاید باهاشون آشنا باشی.
مثلاً یه تابع بازگشتی این شکلیه:
حالا یه تایپ بازگشتی اینجوریه:
تابع addUpTo خودشو صدا میزنه، هر بار با یه مقدار کمتر برای n تا برسه به حالت پایه (فرض میکنیم n عدد غیرمنفی باشه، وگرنه خطای “Maximum call stack size exceeded” میده).
تایپ JSONValue هم یه تایپ یونیونه که تو یکی از حالتهاش یه شیء با کلیدهای رشتهای داره و مقدارش باز خودش JSONValueه.
برای مثال، فرض کن یه شیء person داریم با ویژگیهای name، age و friends:
اینجا friends یه شیءه که کلیداش شمارهان و مقادیرش ساختاری مثل خود person دارن. پس میتونیم یه تایپ Person تعریف کنیم که توش friends یه شیءه با مقادیری از جنس خود Person:
یه نکته: تو جاوااسکریپت، کلیدهای شیء یا رشتهان یا سمبل. حتی اگه کلید رو عدد تعریف کنی، آخرش به رشته تبدیل میشه.
تو این مقاله، نهتنها نگا میکنیم تایپهای بازگشتی چیان، بلکه میبینیم چطور توی ساختارهای داده بازگشتی استفاده میشن و تو دو تا سناریوی واقعی به چه دردی میخورن. اینا ابزارای خفنیان برای موقعیتهای خاص، مثلاً وقتی بخوای یه ابزار تایپ رو گسترش بدی یا تایپ داخلی یه آرایه چندبعدی رو پیدا کنی.
بریم گشتی بزنیم!
بهترین راه فهمیدن تایپهای بازگشتی، نگاه کردن به ساختارهای دادهای مثل درخته:
یه مثال ساده بزنیم: یه گره تو یه درخت دودویی (Binary Tree) حداکثر دو تا بچه داره: یکی چپ، یکی راست. اگه خود این بچهها هم بچه داشته باشن، انگار ریشه یه زیر-درختن.
میتونیم یه تایپ TreeNode برای درخت دودویی بسازیم:
این یه تایپ جنریکه، یعنی value میتونه هر چیزی که بهش بدیم باشه. بچههای چپ و راست هم یا خودشون TreeNode هستن یا null.
فرض کن یه درخت دودویی داریم این شکلی:
(اینجا چون متن اصلی یه مثال تصویری یا توضیح خاص برای درخت نداره، منم ادامه میدم با فرض یه مثال ساده)
مثلاً یه درخت که گره ریشهش یه عدد داره، مثلاً 5، و بچه چپش گرهای با مقدار 3 و بچه راستش گرهای با مقدار 7ه. خود این گرههای 3 و 7 هم میتونن بچههای دیگه داشته باشن یا نداشته باشن (null باشن). تو تایپ بالا، این ساختار با TreeNode<number> تعریف میشه.
اینجا TreeNode یه تایپ بازگشتیه، چون left و right میتونن باز خودشون TreeNode باشن. این باعث میشه بتونیم درختهایی با عمق دلخواه بسازیم.
تایپهای بازگشتی مثل این برای ساختارهای پیچیدهای مثل درختها یا لیستهای پیوندی (Linked Lists) خیلی بهدرد میخورن. مثلاً برای لیست پیوندی:
اینجا هر گره یه مقدار داره و یه اشارهگر به گره بعدی، که یا یه گره دیگهست یا null.
بذار با همون مثال درخت دودویی که دادی شروع کنیم. یه درخت دودویی داریم که با تایپ TreeNode تعریفش کردیم:
type TreeNode<T> = {
و حالا یه نمونه درخت دودویی با مقادیر عددی (تایپ number) اینجوریه:
اینجا چون TreeNode یه تایپ جنریکه، گفتیم که value از نوع number باشه. ساختار بازگشتی این تایپ باعث میشه بتونیم یه درخت با هر عمقی بسازیم، چون left و right خودشون میتونن TreeNode باشن یا null.
برای لیست پیوندی هم یه تایپ مشابه داریم:
اینجا هر گره یه value داره و یه next که یا به یه گره دیگه اشاره میکنه یا nullه. مثلاً اگه یه لیست پیوندی داشته باشیم که به این شکل باشه:
بذار ادامه بدیم و با همون مثال لیست پیوندی که دادی شروع کنیم. لیست پیوندی رو با تایپ LinkedList اینجوری تعریف کردیم:
اینجا یه لیست پیوندی داریم: 1 -> 2 -> 3 -> null. تایپ LinkedList هم که قبلاً تعریفش کردیم، یه تایپ بازگشتیه:
یه نکته باحال: به جای type، میتونیم از interface هم استفاده کنیم. مثلاً همون تایپهای TreeNode و LinkedList رو میشه اینجوری نوشت:
فرقشون چیه؟ type و interface تو خیلی موارد شبیه همن، ولی interface بیشتر برای تعریف ساختار شیءها استفاده میشه و یه کم انعطافپذیری کمتر داره (مثلاً نمیتونی با interface یونیون تایپ بسازی). ولی برای اینجور ساختارهای بازگشتی، هر دو خوب کار میکنن.
استفاده از تایپهای بازگشتی برای ساختارهای دادهای مثل درخت و لیست پیوندی یه کم بدیهیه و شاید خیلی هیجانانگیز نباشه. ولی یه جای باحالتر که میتونیم از بازگشتی بودن استفاده کنیم، تو تایپهای نگاشتی (Mapped Types) و **تایپهای شرطی (Conditional Types)**ه.
تایپهای نگاشتی راهیان برای ساختن یه تایپ جدید بر اساس یه تایپ دیگه. مثلاً میتونیم کلیدهای یه شیء رو بگیریم و یه تایپ جدید بسازیم که همون کلیدها رو داره، ولی نوع مقادیرش فرق داره.
فرض کن یه شیء colors داریم که اسم رنگها و کد هگز اونا رو نگه میداره:
حالا میخوایم یه تایپ بسازیم که همون کلیدها رو داشته باشه، ولی به جای رشته، مقادیرش ازFه بولین باشن:
اینجا ColorsToBoolean یه تایپ نگاشتیه که کلیدهای شیء colors رو میگیره و به هر کدوم یه مقدار boolean نسبت میده. تایپ Result میشه یه چیزی مثل این:
یعنی یه تایپ که کلیداش همون رنگها هستن، ولی مقادیرش booleanه.
بذار با زبون خودمونی بریم تو دل تایپهای نگاشتی و شرطی بازگشتی و ببینیم چه اتفاقی میافته. بعدش هم دو تا کاربرد باحال (DeepPartial و UnwrapArray) رو بررسی میکنیم و آخرش یه نتیجهگیری با یه هشدار کوچولو داریم.
برای ساختن یه تایپ نگاشتی بازگشتی، باید یه جوری به خود همون تایپی که داریم تعریف میکنیم اشاره کنیم. مثلاً:
اینجا یه تایپ Recursive ساختیم که کلیدهای T رو میگیره و برای هر کلید، باز خودشو روی نوع مقدارش (T[K]) صدا میزنه. این یعنی یه جور بازگشتی بودن.
حالا بیایم یه نگا به تایپهای شرطی بندازیم. اینا شبیه عبارات شرطی با عملگر سهتایی (ternary operator) تو جاوااسکریپتن:
مثل اینه که تو جاوااسکریپت بنویسی:
حالا میتونیم تایپ نگاشتی و شرطی رو قاطی کنیم تا یه تایپ نگاشتی شرطی بازگشتی بسازیم:
اینجا داریم میگیم: برای هر کلید تو T، اگه نوع مقدارش (T[K]) عدد بود، همونو برگردون، وگرنه باز خود Recursive رو روش صدا کن. اینجوری یه تایپ بازگشتی ساختیم که فقط برای اعداد متوقف میشه.
یه سناریوی خفن برای تایپهای بازگشتی، گسترش دادن ابزار Partial تو تایپاسکریپته. فرض کن یه اینترفیس برای یه مقاله داریم:
ابزار Partial همه ویژگیهای یه تایپ رو اختیاری (optional) میکنه. مثلاً:
ولی یه مشکل داریم! این کد خطا میده چون age تو author اجباریه و ما نذاشتیمش. Partial فقط ویژگیهای سطح اول رو اختیاری میکنه، نه چیزایی که تو عمق (مثل author.age) هستن.
برای حل این مشکل، میتونیم از یه تایپ بازگشتی استفاده کنیم تا همه ویژگیها، حتی تو عمق، اختیاری بشن:
اینجا DeepPartial میگه: هر ویژگی تو T اختیاریه (?)، و اگه مقدارش یه شیء بود (T[K] extends object)، باز DeepPartial رو روش صدا کن، وگرنه همون نوع اصلی (T[K]) رو نگه دار.
حالا اگه کد مقاله رو با DeepPartial تست کنیم:
دیگه خطا نمیده! چون حالا همه ویژگیها، حتی age تو author، اختیاری شدن.
یه کاربرد دیگه وقتیه که بخوایم نوع داخلی یه آرایه چندبعدی رو پیدا کنیم. مثلاً اگه یه آرایه تودرتو داشته باشیم، چطور میتونیم نوع داخلشو دربیاریم؟
اینجاست که تایپ UnwrapArray به کار میاد:
اینجا UnwrapArray یه تایپ جنریکه. اگه A یه آرایه باشه (A extends Array<infer T>)، نوع داخلش (T) رو میگیره و دوباره UnwrapArray رو روش صدا میکنه. اگه آرایه نبود، همون A رو برمیگردونه.
کلمه infer اینجا برای استخراج نوع داخل آرایه استفاده میشه و فقط تو تایپهای شرطی با extends کار میکنه.
مثال:
یا اگه با کلمه Array بنویسیم:
اینجا UnwrapArray لایهلایه آرایه رو باز میکنه تا برسه به نوع اصلی که stringه.
جهان تایپهای بازگشتی تو تایپاسکریپت خیلی جذابه و پر از قدرته. ولی همونطور که مستندات خود تایپاسکریپت میگه: «با قدرت زیاد، مسئولیت زیاد میاد!»
این تایپها میتونن زمان بررسی تایپ رو طولانی کنن یا اگه زیادی پیچیده باشن، حتی خطای کامپایل بهت بدن. مستندات حتی پیشنهاد میکنه اگه میتونی، اصلاً ازشون استفاده نکن!
پس این همه یادگیری الکی بود؟ نه! بازگشتی بودن یه ابزار قویه و همونطور که تو این مقاله دیدیم، تو موقعیتهای خاص (مثل DeepPartial یا UnwrapArray) خیلی بهدرد میخوره. فقط باید با احتیاط و عاقلانه ازش استفاده کنی.
بیشتر بخوانید: “PocketBase چیست؟“
در خبرنامه ما مشترک شوید و آخرین اخبار و به روزرسانی های را در صندوق ورودی خود مستقیماً دریافت کنید.
دیدگاه بگذارید