تعریف Generic در توابع و constraint کردن آن‌ها

Defining Generic in Functions and Constraint Them

13 مرداد 1399
تعریف Generic در توابع و constraint کردن آن ها

در قسمت قبل کار با Generic ها را مشاهده کردیم اما ما می توانیم Generic های خودمان را در توابع و کلاس ها تعریف کنیم. من با توابع شروع می کنم. فرض کنید تابعی داشته باشیم که دو شیء را با هم ادغام کند:

function merge (objA: object , objB: object) {
  return Object.assign(objA, objB);
}

این تابع دو شیء objA و objB را می گیرد و با استفاده از دستور assign آن ها را با یکدیگر ادغام می کند. همانطور که می بینید در قسمت پارامترهای ورودی نوع هر دو پارامتر را object گذاشته ایم. این کد یک کد صحیح و بدون مشکل است و برای اثبات این مسئله می توانیم یک بار آن را امتحان کنیم:

console.log(merge({ name: 'Max'}, { age: 30 }))

با اجرای دستور بالا، در کنسول مرورگر شاهد یک شیء هستیم که خصوصیات name و age را دارد. البته مشکل کوچکی وجود دارد. اگر بخواهیم نتیجه را روی یک متغیر ذخیره کرده و سپس به خصوصیات name و age دسترسی داشته باشیم با خطا مواجه می شویم:

function merge (objA: object , objB: object) {
  return Object.assign(objA, objB);
}

const mergeObj = merge({ name: 'Max'}, { age: 30 });
mergeObj.name;

کد بالا برای دسترسی mergeObj.name خطای زیر را به ما می دهد:

Property 'name' does not exist on type 'object'.ts

در واقع نباید مشکلی به وجود بیاید اما تایپ اسکریپت نمی تواند تشخیص دهد که چنین خصوصیاتی درون این شیء وجود دارد. ما به تایپ اسکریپت گفته ایم که دو شیء را به این تابع می دهیم و این تابع نیز یک شیء ادغام شده را به ما برمی‌گرداند اما نگفته ایم که این شیء برگردانده شده چه چیز هایی درون خود دارد.

یکی از راه های حل این مشکل استفاده از type casting است:

const mergeObj = merge({ name: 'Max'}, { age: 30 }) as {name: string, age: number};

اما این راه حل آنقدرها هم کاربردی نیست چرا که اگر قرار باشد هر بار از Type casting استفاده کنیم، کدهای خود را بسیار شلوغ کرده و از طرفی وقت خود را نیز تلف می کنیم. راه حل بهتر استفاده از generics است! ما باید تابع خود را تبدیل به یک تابع generic کنیم:

function merge <T, U> (objA: T , objB: U) {
  return Object.assign(objA, objB);
}

const mergeObj = merge({ name: 'Max'}, { age: 30 });

با اضافه کردن angle brackets ها (علامت های <>) دو تایپ generic به نام های دلخواه را مشخص می کنیم (من T و U را انتخاب کرده ام). سپس دو پارامتر این تابع را از نوع T و U گذاشته ایم. حالا این یعنی چه؟ اگر موس خود را روی نام تابع (merge) ببرید تایپ اسکریپت پیام زیر را به شما نشان می دهد:

function merge<T, U>(objA: T, objB: U): T & U

یعنی با این کار تایپ اسکرپیت فهمیده است که این تابع T و U را برمی گرداند، بنابراین متوجه شده است که T و U دو چیز جدا هستند که در جواب تابع ما وجود خواهند داشت. همچنین اگر موس را روی mergeObj ببرید پیام زیر برایتان نمایش داده می شود:

const mergeObj: {
name: string;
} & {
age: number;
}

یعنی تایپ اسکریپت می فهمد که خروجی ما دارای name و age خواهد بود که به ترتیب رشته و عدد هستند! در حالت قبل می گفتیم تایپ پارامترها object (شیء) است که توصیفی بسیار کلی است! یک شیء می تواند مفهوم گسترده ای داشته باشد و دیگر مشخص نیست چه خصوصیاتی دارد اما generic ها به طور واضح تری این مسئله را مشخص می کنند. البته در حال حاضر فقط به تایپ اسکریپت گفته ایم که پارامترهای ورودی تابع merge از دو تایپ مختلف هستند و چیزی در مورد شیء بودن آن ها (به صورت صریح) نگفته ایم.

گفته بودم که Generic به معنای «کلی» یا «عمومی» است و هدف ما هم در این قسمت همین است، اینکه توصیفی کلی از پارامترهای ورودی داشته باشیم. اگر بخواهیم به صورت دقیق و خاص تایپ هر کدام از اشیاء را مشخص کنیم به مشکل برمی خوریم. به طور مثال:

function merge (objA: {name: string} , objB: {age: number}) {
  return Object.assign(objA, objB);
}

این تابع دیگر به درد نمی خورد چرا که فقط اشیائی را در هم ادغام می کند که دقیقا مطابق با تعریف ما باشد و مثلا اگر شیء ما خصوصیت دیگری نیز داشته باشد خطا می دهد:

const mergedObj = merge({ name: 'Max', hobbies: ['Sports'] }, { age: 30 });

چرا؟ به دلیل اینکه hobbies دیگر بر اساس تعریف شیء قبلی نیست و ما فقط توصیفی کلی نیاز داریم که در زمان فراخوانی تایپ خود را مشخص کند. مثلا در کد بالا دو شیء را به merge داده ایم که اولی دو خصوصیت و دومی یک خصوصیت دارد. بر همین اساس تایپ اسکریپت می تواند تایپ mergedObj را مشخص کند تا بعدا بتوانیم این خصوصیت ها را روی آن صدا بزنیم (مثلا mergedObj.name).

حالا تصور کنید که به جای شیء دوم فقط عدد 30 را پاس بدهیم:

const mergedObj = merge({ name: 'Max', hobbies: ['Sports'] }, 30);

اگر mergedObj را console.log کنیم اصلا عدد 30 را در نتیجه نهایی نمی بینیم. چرا؟ به دلیل اینکه دستور assing دو شیء می خواهد اما ما فقط عدد 30 را پاس داده ایم که شیء نیست و به طور کل نادیده گرفته می شود (فقط hobbies و name را خواهیم داشت). جالب تر اینجاست که خطایی به ما داده نمی شود و اصلا نمی فهمیم که کد خود را اشتباه نوشته ایم.

راه حل اینجاست که به تایپ اسکریپت بگوییم پارامترهای تابع merge باید حتما شیء باشند، جزئیات دقیق آن و اینکه چه نوع شیء ای باشند مهم نیست، اما حتما باید شیء باشند. در حال حاضر فقط گفته ایم T و U که یعنی هر تایپی که بخواهند باشند. راه حل این مسئله با استفاده از قابلیتی به نام constraints انجام می شود:

function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

با اضافه کردن کلیدواژه extends و سپس object، می گوییم که T و U باید شیء باشند. حالا دیگر نمی توانیم عدد 30 خالی را پاس بدهیم بلکه باید حتما یک شیء داشته باشیم. البته به جای extends object می توانید از هر تایپ دیگری مثل union type ها نیز استفاده کنید (مثلا extends string | number).

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش جامع تایپ اسکریپت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

ما را دنبال کنید
اینستاگرام روکسو تلگرام روکسو ایمیل و خبرنامه روکسو