تمرین بیشتر با Generic Function ها + نوع خاصی از Constraint

Practice More with Generic Functions

13 مرداد 1399
تمرین بیشتر با generic function ها + نوع خاصی از constraint

در قسمت قبل با generic function ها یا توابعی که از تایپ های generic استفاده می کنند به خوبی آشنا شدیم اما از آنجایی که معمولا مبحث generic function ها کمی عجیب و ناآشناست، تصمیم گرفتم یک مثال دیگر از آن را نیز حل کنیم تا با این مبحث بیشتر آشنا شویم.

فرض کنید تابعی به نام countAndDescribe داشته باشیم:

function countAndDescribe<T>(element: T) {

}

من می خواهم کاری کنم که این تابع یک tuple را برگرداند. این tuple حاوی خود element و رشته ای توصیفی از آن خواهد بود که تعداد کاراکتر های element را مشخص می کند. بنابراین در ابتدا یک رشته پیش فرض را تعریف می کنم که اگر element خالی بود آن را چاپ  کنیم:

function countAndDescribe<T>(element: T) {
  let descriptionText = 'Got no value.';
  return [element, descriptionText];
}

حالا باید با یک شرط دیگر چک کنیم که اگر طول element بیشتر از صفر بود متغیر descriptionText تغییر کند:

function countAndDescribe<T>(element: T) {
  let descriptionText = 'Got no value.';
  if (element.length === 1) {
    descriptionText = 'Got 1 element.';
  } else if (element.length > 1) {
    descriptionText = 'Got ' + element.length + ' elements.';
  }
  return [element, descriptionText];
}

منطق این کد بسیار ساده است بنابراین توضیح اضافه نمی دهم اما در همین ابتدا تایپ اسکریپت به ما خطا می دهد که نمی توانی از element.length استفاده کنی چرا که اصلا مشخص نیست element چیزی باشد. برای مشخص کردن این موضوع این بار از یک interface استفاده می کنم:

interface Lengthy {
  length: number;
}

در مرحله بعد از این interface به عنوان یک constraint استفاده می کنیم:

function countAndDescribe<T extends Lengthy>(element: T) {
  let descriptionText = 'Got no value.';
  if (element.length === 1) {
    descriptionText = 'Got 1 element.';
  } else if (element.length > 1) {
    descriptionText = 'Got ' + element.length + ' elements.';
  }
  return [element, descriptionText];
}

با دستور extends Lengthy به تایپ اسکریپت می گوییم که پارامتر ورودی ما (که قرار است یا تابع باشد یا رشته) حتما دارای length خواهد بود. حالا خطای element.length از بین می رود و می توانیم به سادگی این کد را تست کنیم:

console.log(countAndDescribe('Hi there!'));

با اجرای این دستور هیچ خطایی دریافت نمی کنیم. در حال حاضر تایپ اسکریپت می فهمد که خروجی تابع ما یک آرایه از نوع رشته یا T خواهد بود. برای اینکه دقیق تر باشیم خودمان دستی مشخص می کنیم که خروجی تابع یک tuple خواهد بود:

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
  let descriptionText = 'Got no value.';
  if (element.length === 1) {
    descriptionText = 'Got 1 element.';
  } else if (element.length > 1) {
    descriptionText = 'Got ' + element.length + ' elements.';
  }
  return [element, descriptionText];
}

console.log(countAndDescribe(['Sports', 'Cooking']));

قبلا در مورد نحوه تعیین return type صحبت کردیم و مطمئن هستم که شما نیز آن را بلد هستید. چرا گفته ایم return type ما یک آرایه است که عضو اول آن از تایپ T و دومی رشته است؟ قبلا هم گفته بودم که اولین عضوی که تابع countAndDescribe برمی گرداند خود element خواهد بود بنابراین از T استفاده می کنیم که یک تایپ generic است و نمی خواهیم اطلاعات خاص آن را توصیف کنیم. عضو دوم نیز رشته است که همان متغیر descriptionText خواهد بود. همچنین می بینید که در خط آخر به countAndDescribe یک آرایه پاس داده ام که دو عضو دارد و اگر دستور بالا را اجرا کنیم چنین چیزی را در کنسول مرورگر مشاهده خواهیم کرد:

مقادیر Sports و cooking در کنسول مرورگر
مقادیر Sports و cooking در کنسول مرورگر

همچنین اگر یک آرایه خالی را به آن پاس بدهید عبارت Got no value را می گیرید اما نمی توانید مثلا عدد 10 را به آن پاس بدهید چرا که خصوصیت length برای اعداد بی معنی است و ما هم آن را در پارامتر خودمان تعریف نکرده ایم. امیدوارم با ایده کلی تایپ های Generic آشنا شده باشید. مفهوم اصلی اینجاست که ما می خواهید داده ای را پاس بدهیم ولی در عین حال خصوصیات و جزئیات دقیق آن داده برای ما اهمیت ندارد بلکه شکل کلی و عمومی آن است که در کار ما اهمیت پیدا می کند.

قبل از به پایان رساندن این جلسه باید با یک نوع خاص از constraint ها نیز آشنا شویم. فرض کنید تابعی به نام extractAndConvert داشته باشیم که پارامتر اول آن یک شیء و پارامتر دوم آن یک key باشد:

function extractAndConvert(obj, key) {
  return 'Value: ' + obj[key];
}

کار این تابع این است که key را برابر key شیء دریافتی گرفته و مقدار آن را به ما بدهد. به زبان ساده تر قرار است بر اساس یک key، مقداری را از شیء obj برگرداند. بنابراین می توانیم تایپ پارامترها را مشخص کنیم:

function extractAndConvert(obj: object, key: string) {
  return 'Value: ' + obj[key];
}

تایپ را در این کد مشخص کردیم اما به مشکل می خوریم و تایپ اسکریپت به ما خطا می دهد. احتمالا می توانید حدس بزنید خطا به چه دلیل است. تایپ اسکریپت به ما می گوید که اصلا معلوم نیست key پاس داده شده برای شیء obj وجود داشته باشد! از کجا معلوم که کاربر رشته aslihasilfhasldh را به ما پاس ندهد؟ چنین key احتمالا در هیچ کدام از اشیاء ما وجود نخواهد داشت! به طور مثال اگر تابع را به شکل زیر صدا بزنیم چه می شود؟

extractAndConvert({}, 'name');

آیا متوجه مشکل می شوید؟ شیء ما یک شیء خالی است و ازطرفی تابع باید خصوصیت name را از درون آن خارج کند! خطای تایپ اسکریپت نیز مربوط به چنین حالت هایی است. ما باید تایپی تعریف کنیم که key تایپ دیگر باشد! یک روش برای این کار وجود دارد:

function extractAndConvert<T extends object, U extends keyof T>(
  obj: T,
  key: U
) {
  return 'Value: ' + obj[key];
}
extractAndConvert({ name: 'Max' }, 'name');

در این کد مثل همیشه generic type های خود را تعریف کرده ایم و سپس برای U گفته ایم که keyof T را extend می کند یعنی باید یک key از شیء T باشد. بدین صورت مشکل را حل می کنیم!

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

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

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

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