آموزش React: استفاده از Typescript generic در ری اکت
عباس سپهوند
عباس سپهوند
  • 1402/03/04
  • 25 دقیقه
  • 0 نظر

آموزش React: استفاده از Typescript generic در ری اکت

در این مقاله، به بررسی مفهوم و استفاده از generic در تایپ‌اسکریپت و ری‌اکت می‌پردازیم. این مفهوم که برای بسیاری از برنامه‌نویسان همیشه یک چالش بوده است، در اینجا با استفاده از مثال‌های واضح شرح داده شده است. امیدواریم که این مطالب به شما در پروژه‌های آینده کمک کند تا از این مفهوم پرکاربرد بهره‌برداری کنید.

هربار که سعی می کردم مستندات TypeScript را بخوانم، چند روزی بیشتر نمی توانم به مطالعه مطالب آن ادامه دهم. می‌خواهم یکی از مفاهیم مهم TypeScript را به گونه‌ای بازنویسی کنم که واقعاً قابل درک برای یک خواننده عادی باشد  بیایید با یکی ازپر چالش ترین مفاهیم که بسیاری از توسعه دهندگان با آن مواجه هستند شروع کنیم: ژنریک‌ها! و ما قصد داریم با رویکردی پایین به بالا شروع کنیم: بیایید یک کامپوننت را بدون استفاده از ژنریک‌ها پیاده‌سازی کنیم.

قبل از شروع با توجه به اینکه همیشه توصیه کردم که مفاهیم مهم را در قالب پیاده سازی اپلیکیشن های واقعی یاد بگیرید می توانید در دوره کاملا رایگان آموزش ری اکت شرکت کنید. همینطور برای اینکه دانش تون رو به سطح بالاتری از ری اکت ارتقا بدهید در دوره آموزش پیشرفته ری اکت (پروژه پنل ادمین) شرکت کنید.

 

فرض کنید می خواهیم یک select component پیاده سازی کنیم. این کامپوننت باید این قابلیت را داشته باشد که آرایه ای از مقادیر را به عنوان ورودی دریافت کند. همچنین باید event ی را تعریف کنیم تا با انتخاب هر یک از آیتم های این کامپوننت، این event اجرا شود. پیاده سازی زیر را در نظر بگیرید:

 

import React from 'react';

type SelectOption = {
  value: string;
  label: string;
};

type SelectProps = {
  options: SelectOption[];
  onChange: (value: string) => void;
};

export const Select = ({ options, onChange }: SelectProps) => {
  return (
    <select onChange={(e) => onChange(e.target.value)}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
};

 

اکنون می توانیم از پیاده سازی فوق استفاده مجدد (re-use) کنیم:

 

<>
  <Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
  <Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>

 

اما متاسفانه، به محض اینکه اپلیکیشن بزرگ و بزرگ تر می شود، چندین مسئله به وجود می آید:

  1. اولین مشکل این است که کامپوننت select فقط برای یک فرمت خاص پیاده سازی شده است. کامپوننتی که از کامپوننت select استفاده می کند لازم است به فرمت مورد نظر خود آن را تبدیل کند. از آنجا که ممکن است این کامپوننت در بخش های مختلفی از اپلیکیشن استفاده شود، convert کردن آن به یک فرمت خاص باعث کاهش خوانایی و نگهداری کد می شود
  2. رویداد onChange فقط خصوصیت id آیتم انتخاب شده را بر می گرداند. در این حالت برای اینکه بخواهیم به نام آیتم دسترسی داشته باشیم باید آرایه مبدا را بر اساس id فیلتر کنیم تا به نام آیتم برسیم. 
  3. این کامپوننت به طور کامل type safe نیست. و خیلی ساده امکان ایجاد باگ در آن وجود دارد. به فرض مثال می توانید از تابع doSomethingWithBooks  برای moviesOptions استفاده کنید که اشتباه است.

 

بنابراین لازم است تغییراتی در کامپوننت ایجاد کنیم.

 

دوره تخصصی آموزش React

برای دسترسی به دوره 6 ساعته رایگان آموزش ری اکت کلیک کنید

بهبود کامپوننت

می خواهیم این تغییرات را در کامپوننت ایجاد کنیم:

  • تمام کدهایی که باعث فیلتر شدن آرایه برای دسترسی به نام آیتم می شود را حذف کنیم
  • کامپوننت را type-safe کنیم به طوری که اگر اشتباها function اشتباهی را به رویداد onChange ارسال کردیم با خطا مواجه شویم.

برای رسیدن به این اهداف لازم در کامپوننت select چنین تغییراتی را ایجاد کنیم:

  • آرایه ای از مقادیر مشخص را دریافت کند و آن را به select option ها تبدیل کند
  • رویداد onChange لازم است به جای id، کل دیتای انتخاب شده را برگرداند
  • باید ارتباطی بین options و onChange ایجاد شود به طوری که اگر به فرض مثال doSomethingWithBooks مقادیر از نوع Movie دریافت کرد، جلوی آن گرفته شود.

 

در ابتدا می دانیم که باید type های لازم را تعریف کنیم:

 

export type Book = {
  id: string;
  title: string;
  author: string; // only books have it
};

export type Movie = {
  id: string;
  title: string;
  releaseDate: string; // only movies have it
};
... // all other types for the shop goods

 

برای رسیدن به این اهداف بهترین انتخاب استفاده از generic type ها در تایپ اسکریپت است. به طور کلی generic ها یک placeholder برای یک تایپ خاص است. به طور مثال تعریف زیر را در نظر بگیرید:

 

function identity<Type>(a: Type): Type {
  return a;
}

 

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

 

const a = identity<string>("I'm a string") // "a" will be a "string" type
const b = identity<boolean>(false) // "b" will be a "boolean" type

 

هر گونه تلاش برای ارسال مقداری با تایپ ناسازگار باعث خطا می شود:

 

const a = identity<string>(false) // typescript will error here, "a" can't be boolean
const b = identity<boolean>("I'm a string") // typescript will error here, "b" can't be string

 

با توجه به توضیحات فوق برای بازنویسی کامپوننت select به این شکل می توانیم به صورت زیر عمل کنیم:

 

type GenericSelectProps<TValue> = {
    values: TValue[];
    onChange: (value: TValue) => void;
};

export const GenericSelect = <TValue>({values, onChange}: GenericSelectProps<TValue>) => {
    // all the code that we had in BookSelect
};

 

اما در کد فوق مشکلی وجود دارد. در واقع از آنجا که یک React component داریم، تایپ اسکریپت تصور می کند که اولین <TValue> یک jsx element است در نتیجه با خطا مواجه می شویم. نکته دیگر اینکه از آنجا که تایپ ارسالی به این کامپوننت فعلا مشخص نشده است (تایپ در زمان استفاده مشخص می شود) تایپ اسکریپت به مقادیر value.title و value.id دسترسی ندارد. در نتیجه برای حل این دو مورد می توانیم به شکل زیر عمل کنیم:

 

type Base = {
    id: string;
    title: string;
}

export const GenericSelect = <TValue extends Base>({values, onChange}: GenericSelectProps<TValue>) => {
     // all the code that we had in BookSelect
};

 

محدودیت فوق به این دلیل استفاده شده تا تایپ اسکریپت حداقل اطلاعات لازم برای تشخیص خصوصیت های TValue را داشته باشد. در واقع با این روش با تایپ اسکریپت می گوییم. که هیچ ایده ای از اینکه TValue چه ساختاری دارد در واقع نداریم اما حداقل می دانیم همیشه id و title را داریم. با توجه به تغییرات فوق کامپوننت نهایی به شکل زیر خواهد بود:

 

type Base = {
  id: string;
  title: string;
};

type GenericSelectProps<TValue> = {
  values: TValue[];
  onChange: (value: TValue) => void;
};

export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
  const onSelectChange = (e) => {
    const val = values.find((value) => value.id === e.target.value);

    if (val) onChange(val);
  };

  return (
    <select onChange={onSelectChange}>
      {values.map((value) => (
        <option key={value.id} value={value.id}>
          {value.title}
        </option>
      ))}
    </select>
  );
};

 

نهایتا برای استفاده از این کامپوننت داریم:

 

// This select is a "Book" type, so the value will be "Book" and only "Book"
<GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />

// This select is a "Movie" type, so the value will be "Movie" and only "Movie"
<GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />

 

در پایان یادآوری می کنم می توانید در دوره کاملا رایگان آموزش ری اکت شرکت کنید. همینطور برای ارتقاء سطح دانش ری اکت می توانید در دوره آموزش پیشرفته ری اکت (پروژه پنل ادمین) شرکت کنید.

 

دیدگاه

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

ورود یا ثبت نام
عباس سپهوند
عباس سپهوند

برنامه نویس و توسعه دهنده نرم افزار

مشاهده پروفایل
5 مقاله اخیر

آموزش React: راهنمای کامل useCallback

عباس سپهوند
زمان مطالعه: 40

آموزش React: راهنمای کامل useEffect

عباس سپهوند
زمان مطالعه: 25

آموزش React: راهنمای کامل Ref ها در React

عباس سپهوند
زمان مطالعه: 15
مشاهده همه مقالات