آنچه که ممکن است در تایپ اسکریپت به عنوان Generic درنظر بگیریم در واقع سه مفهوم مجزا است که عبارتند از:

// ۱٫ Passing types to types
type PartialUser = Partial<{ id: string; name: string }>;
// ۲٫ Passing types to functions
const stringSet = new Set<string>();
// ۳٫ Inferring types from arguments passed to functions
const objKeys = <T extends object>(obj: T): Array<keyof T> => {
  return Object.keys(obj) as Array<keyof T>;
};
const keys = objKeys({ a: 1, b: 2 });
//    ^? const keys: ("a" | "b")[]

انتقال تایپ به تایپ

با انتقال تایپ به تایپ شروع می‌کنیم.

در تایپ اسکریپت، ما می‌توانیم تایپی را تعریف کنیم که نشان‌دهنده یک آبجکت، primitive، تابع یا هر چیز دیگری که می‌خواهیم، باشد.

export type User = {
  id: string;
  name: string;
};
export type ToString = (input: any) => string;

اما فرض کنید باید چند تایپ با ساختار مشابه ایجاد کنیم. به عنوان مثال، یک data shape.

مثال زیر را در نظر بگیرید:

type ErrorShape = {
  message: string;
  code: number;
};
type GetUserData = {
  data: {
    id: string;
    name: string;
  };
  error?: ErrorShape;
};
type GetPostsData = {
  data: {
    id: string;
    title: string;
  };
  error?: ErrorShape;
};
type GetCommentsData = {
  data: {
    id: string;
    content: string;
  };
  error?: ErrorShape;
};

اگر مایل باشیم که از OOP استفاده کنیم، می‌توانیم این کار را با استفاده از یک interface با قابلیت استفاده مجدد مانند کد زیر انجام دهیم:

interface DataBaseInterface {
  error?: ErrorShape;
}
interface GetUserData extends DataBaseInterface {
  data: {
    id: string;
    name: string;
  };
}
interface GetPostsData extends DataBaseInterface {
  data: {
    id: string;
    title: string;
  };
}
interface GetCommentsData extends DataBaseInterface {
  data: {
    id: string;
    content: string;
  };
}

اما ایجاد یک type function مختصرتر است، که تایپ داده را دریافت کرده و data shape جدید را برمی‌گرداند:

// Our new type function!
type DataShape<TData> = {
  data: TData;
  error?: ErrorShape;
};
type GetUserData = DataShape<{
  id: string;
  name: string;
}>;
type GetPostsData = DataShape<{
  id: string;
  title: string;
}>;
type GetCommentsData = DataShape<{
  id: string;
  content: string;
}>;

درک این سینتکس مهم است، زیرا در ادامه مقاله با آن سروکار خواهیم داشت.

براکت‌های موجود در <TData>به تایپ اسکریپت می‌گویند که ما می‌خواهیم یک تایپ آرگومان به این تایپ اضافه کنیم. ما می‌توانیم TDataرا هر چیزی نام‌گذاری کنیم، این دقیقاً مانند آرگومان یک تابع می‌باشد.

مثالی که در ادامه داریم یک تایپ generic است:

// Generic type
type DataShape<TData> = {
  data: TData;
  error?: ErrorShape;
};
// Passing our generic type
// another type
type GetPostsData = DataShape<{
  id: string;
  title: string;
}>;

تایپ‌های generic می‌توانند چندین نوع آرگومان را بپذیرند. ما می‌توانیم پیش‌فرض‌ها و همچنین محدودیت‌هایی را برای تایپ آرگومان‌ها تنظیم کنیم تا فقط بتوانیم انواع خاصی را به آن‌ها ارسال کنیم.

اما فقط تایپ‌ها نیستند که می‌توانیم تایپ را به آن‌ها منتقل کنیم:

const createSet = <T>() => {
  return new Set<T>();
};
const stringSet = createSet<string>();
// ^? const stringSet: Set<string>
const numberSet = createSet<number>();
// ^? const numberSet: Set<number>

صحیح است، درست مانند تایپ‌ها که می‌توانند تایپ‌های دیگر را به عنوان آرگومان بپذیرند، توابع نیز می‌توانند این کار را انجام دهند.

انتقال تایپ به توابع

در این مثال، زمانی که createSet را تعریف می‌کنیم، یک <T>را به قبل از پرانتز اضافه می‌نماییم. سپس آن <T>را به صورت دستی به Set()انتقال می‌دهیم، که به ما اجازه می‌دهد تا تایپ آرگومان را ارسال کنیم:

export const createSet = <T>() => {
  return new Set<T>();
}

این بدان معناست که وقتی آن را فراخوانی می‌کنیم، می‌توانیم یک تایپ آرگومان <string>را به createSetارسال کنیم.

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

const stringSet = createSet<string>();
// Argument of type 'number' is not assignable to parameter of type 'string'.
stringSet.add(123);

این موضوع دومین چیزی است که هنگام صحبت در مورد genericها به آن اشاره می‌شود: انتقال دستی تایپ‌ها به توابع.

اگر از React استفاده کنیم این را خواهیم دید، و ما باید یک تایپ آرگومان را به useStateارسال کنیم:

const [index, setIndex] = useState<number>(0);
// ^? const index: number

اما متوجه رفتار دیگری در React می‌شویم که در برخی موارد، برای استنباط آن نیازی به ارسال تایپ آرگومان نداریم:

const [index, setIndex] = useState(0);
// ^? const index: number
// WHAT IS THIS SORCERY

برای اینکه ببینیم چه اتفاقی افتاده است، بار دیگر تابع createSetرا بررسی می‌کنیم. باید به این نکته توجه داشته باشیم که فقط تایپ‌ آرگومان‌ها را دریافت می‌کند:

export const createSet = <T>() => {
  return new Set<T>();
}

اگر تابع خود را طوری تغییر دهیم که یک مقدار اولیه برای مجموعه بپذیرد، در این صورت چه اتفاقی می‌افتد؟

می‌دانیم که مقدار اولیه باید از نوع Set باشد، بنابراین آن را به صورت Tمی‌نویسیم:

export const createSet = <T>(initial: T) => {
  return new Set<T>([initial]);
}

اکنون وقتی آن را فراخوانی می‌کنیم، باید یک initialارسال کنیم، و این باید دارای همان نوع تایپ آرگومانی باشد که به createSetمی‌فرستیم:

const stringSet = createSet<string>("matt");
//    ^? const stringSet: Set<string>
// Argument of type 'string' is not assignable to parameter of type 'number'.
const numberSet = createSet<number>("pocock");

این اتفاق عجیبی نیست زیرا تایپ اسکریپت می‌تواند تایپ Tرا از initialاستنباط کند. به عبارت دیگر، تایپ آرگومان از آرگومان زمان اجرا استنباط می‌شود.

const stringSet = createSet("matt");
//    ^? const stringSet: Set<string>
const numberSet = createSet(123);
//    ^? const numberSet: Set<number>

می‌توانیم این موضوع را با نگه داشتن ماوس روی یکی از فراخوانی‌های createSetبررسی کنیم. وقتی رشته‌ای را به آن ارسال می‌کنیم، خواهیم دید که <string>استنباط می‌شود:

// hovering over createSet
const createSet: <string>(initial: string) => Set<string>

در useStateنیز همین امر صادق است، برای آن باید به سینتکس useState<number>در tooltip مراجعه کنیم:

// hovering over useState
useState<number>(initialState: number | (() => number)): [number, Dispatch<SetStateAction<number>>]

این موضوع همچنین برای تابع objKeysکه شامل برخی مزایای بیشتر می‌باشد نیز صادق است:

const objKeys = <T extends object>(obj: T) => {
  return Object.keys(obj) as Array<keyof T>;
};

جمع‌بندی

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