آشنایی با Discriminated Union و قابلیت Type Casting

Discriminated Union

12 مرداد 1399
آشنایی با discriminated union و قابلیت Type Casting

Discriminated Union چیست؟

ما در جلسات قبل با type guard ها آشنا شدیم اما انواع خاصی از type guard ها وجود دارد که به discriminated union معروف هستند که به ما در استفاده از type guard ها کمک می کنند. در واقع آن ها یک الگوی خاص از union type ها هستند که پیاده سازی type guard ها را ساده تر می کنند. فرض کنید چند interface داشته باشیم. البته می توانید از کلاس ها هم استفاده کنید، من از interface ها استفاده می کنم تا نشان دهم با آن ها هم سازگار است:

interface Bird {
  flyingSpeed: number;
}

interface Horse {
  runningSpeed: number;
}

پرنده (bird) و Horse (اسب) به ترتیب سرعت پرواز و سرعت دویدن را به عنوان خصوصیت هایشان دارند که باید از جنس عدد باشد. حالا بر همین اساس union type خود را می سازیم:

type Animal = Bird | Horse;

یعنی حیوان مورد نظر ما یا پرنده است یا اسب. در این مرحله می خواهیم تابعی بنویسیم که سرعت حیوان پاس داده شده را نمایش دهد:

function moveAnimal(animal: Animal) {
  console.log('Moving at speed: ' + animal.flyingSpeed);
}

کد بالا غلط است چرا که ممکن است حیوان ما اسب باشد و اسب نیز flyingSpeed (سرعت پرواز) ندارد بلکه سرعت دویدن (runningSpeed) دارد. برای چنین مواقعی type guard ها را داشتیم و با یک شرط if مشکل را حل می کردیم:

function moveAnimal(animal: Animal) {
  if ('flyingSpeed' in animal) {
    console.log('Moving at speed: ' + animal.flyingSpeed);
  }
}

شاید در نگاه اول این کار بدون نقص باشد و در حقیقت نیز اشکالی در استفاده از این روش نیست اما اگر پروژه ما پیچیده تر از این باشد به مشکل برمی خوریم. به طور مثال فرض کنید در همین کد ساده بالا به جای دو نوع حیوان، انواع حیوانات دیگر را نیز به آن اضافه کنیم، در چنین حالتی باید شرط های if پشت سر همی را بنویسیم که کد ما را شلوغ می کند. همچنین اگر flyingSpeed را با کوچکترین غلط املایی بنویسیم برنامه خراب می شود.

در چنین موقعیتی استفاده از discriminated union بسیار کاربردی خواهد بود. برای استفاده از آن چند مرحله وجود دارد. در مرحله اول باید به هر interface یک type بدهید:

interface Bird {
  type: 'bird';
  flyingSpeed: number;
}

interface Horse {
  type: 'horse';
  runningSpeed: number;
}

توجه داشته باشید که هر دو کد بالا interface هستند بنابراین رشته bird یا horse مقداری برای خصوصیت type نیستند بلکه یک literal type هستند. یعنی گفته ایم که type فقط می تواند مقدار bird را بگیرد (یا در interface دوم فقط horse را بگیرد). همچنین شما می توانید به جای type از kind یا هر چیز دیگری استفاده کنید. در مرحله بعد باید از یک دستور Switch استفاده کنیم:

function moveAnimal(animal: Animal) {
  let speed;
  switch (animal.type) {
    case 'bird':
      speed = animal.flyingSpeed;
      break;
    case 'horse':
      speed = animal.runningSpeed;
  }
  console.log('Moving at speed: ' + speed);
}

بدین صورت مشخص کردن خروجی بسیار ساده و منعطف می شود، همچنین به دلیل استفاده از یک دستور switch کدها منظم تر خواهند بود. برای تست کردن این کد می توانیم آن را اجرا کنیم:

moveAnimal({type: 'bird', flyingSpeed: 10});

خروجی بدون نقصل در مرورگر مشاهده خواهد شد. به این ویژگی discriminated union می گوییم.

Type Casting چیست؟

Type casting قابلیتی است که به ما اجازه می دهد در زمانی که تایپ اسکریپت نمی تواند تایپ خاص یک نوع داده را تشخیص دهد، خودمان تایپ خاص را به تایپ اسکریپت اعلام کنیم. یکی از مثال های واضح این مسئله دسترسی به DOM است. به طور مثال فرض کنید در فایل index.html خود یک پاراگراف ساده و خالی داشته باشیم (فقط یک تگ <p>). اگر بخواهیم به این تگ در تایپ اسکریپت دسترسی پیدا کنیم مثل جاوا اسکریپت عادی می گوییم:

const paragraph = document.querySelector('p');

اگر موس خود را روی paragraph ببرید تایپ اسکریپت به شما می گوید که تایپ این داده یا HTMLParagraphElement و یا null است. چرا؟ به دلیل اینکه تایپ اسکریپت می فهمد که querySelector به دنبال تگ <p> می گردد اما معلوم نیست چنین تگی در فایل HTML ما وجود داشته باشد چرا که تایپ اسکریپت به فایل HTML دسترسی ندارد. در این قسمت از آنجا که از خود تگ p برای پیدا کردن آن استفاده کرده ایم تایپ اسکریپت توانسته است که نوع تگ را تشخیص بدهد اما اگر با چیز دیگری مثل id دنبال عنصر HTML بگردیم پیدا کردن آن سخت می شود.

به طور مثال اگر برای همان <p> یک id با مقدار message-output تعریف کنیم و در تایپ اسکریپت از getElementById برای دریافت آن استفاده کنیم، دیگر توانایی تشخیص دقیق از بین می رود و تایپ اسکریپت می گوید تایپ یا null است یا HTMLElement (یعنی عنصر HTML – بدون مشخص کردن اینکه پاراگراف است یا تصویر است یا ...). شاید این مسئله برای یک تگ پاراگراف مهم نباشد اما تصور کنید در فایل index.html خود یک input به جای آن داشته باشیم:

<body>
  <input type="text" id="user-input">
</body>

حالا برای دریافت آن در تایپ اسکریپت می گویم:

const userInputElement = document.getElementById('user-input');

در حال حاضر اگر بخواهیم value را برای آن تعیین کنیم به خطا برمی خوریم:

const userInputElement = document.getElementById('user-input');

userInputElement.value = 'Hi there';

چرا؟ به دلیل اینکه تایپ اسکریپت می گوید شاید userInputElement وجود نداشته و null باشد. برای حل این مشکل از علامت تعجب استفاده می کنیم که در جلسات قبل شاهد آن بودیم:

 const userInputElement = document.getElementById('user-input')!;

اما باز هم خطایی جدید می گیریم که می گوید تایپ userInputElement برابر HTMLElement (عنصر HTML) است و معلوم نیست دقیقا چه عنصری! از طرفی Value یک خصوصیت خاص است که فقط برخی از عناصر HTML می توانند از آن استفاده کنند بنابراین نباید آن را مستقیما روی یک عنصر عمومی که معلوم نیست چه باشد صدا بزنیم.

در اینجا باید به تایپ اسکریپت بگوییم که عنصر دریافتی ما از نوع input است. دو راه برای این کار وجود دارد. روش اول اضافه کردن تایپ به صورت مستقیم و به شکل زیر است:

const userInputElement = <HTMLInputElement>document.getElementById('user-input')!;

اگر نوع عنصر را قبل از دستور (قبل از document) به شکل بالا بیاورید تایپ اسکریپت می فهمد که مشکلی نیست و تایپ حتما از نوع input است. این روش کار می کند اما اگر در فریم ورک react باشید این روش با کد های JSX تداخل ایجاد می کند بنابراین باید به سراغ روش دوم بروید:

const userInputElement = document.getElementById('user-input')! as HTMLInputElement;

در اینجا با استفاده از کلیدواژه as و سپس ذکر تایپ مورد نظر، مطمئن می شویم که تایپ از نوع input تعیین می شود. در نهایت باید در مورد علامت تعجب توضیحاتی بدهم. علامت تعجبی که انتهای دستورات بالا می بینید به تایپ اسکریپت می گوید من به عنوان توسعه دهنده مطمئن هستم که مقدار این دستور هیچ وقت null نمی شود (یعنی عنصر حتما وجود دارد). بنابراین تنها زمانی از علامت تعجب استفاده کنید که به کد ها دسترسی داشته و مطمئن هستید نتیجه null نمی شود، در غیر این صورت از یک شرط if ساده استفاده کنید تا مطمئن شوید عنصر شما وجود داشته و null نمی باشد:

const userInputElement = document.getElementById('user-input');

if (userInputElement) {
  (userInputElement as HTMLInputElement).value = 'Hi there!';
}

در ضمن توجه داشته باشید که اگر تعیین تایپ (type casting) را انجام بدهید نیازی به گذاشتن علامت تعجب نیست چرا که اگر تایپ چیزی را تعیین می کنید حتما مطمئن هستید که آن چیز وجود خارجی دارد.

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

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

.
28 تیر 1400
سلام توی تایپ کستینگ میتونیم اینطور هم تعریف کنیم که شما نگفتید let myP: HTMLParagraphElement = document.querySelector("p")

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

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