در جلسه دوازدهم از دوره آموزش TypeScript از صفر تا پیشرفته، به سراغ یکی از مهم‌ترین و کاربردی‌ترین قابلیت‌های این زبان، یعنی Generics در TypeScript می‌رویم.

اگر با TypeScript کار کرده باشید، احتمالاً با شرایطی مواجه شده‌اید که می‌خواهید یک تابع، Interface یا کلاس را برای چند نوع داده مختلف استفاده کنید، بدون اینکه مجبور باشید برای هر نوع داده کد جداگانه‌ای بنویسید.

اینجاست که Generics به کمک ما می‌آید.

در این مقاله، مفهوم Generic را از پایه بررسی می‌کنیم و با مثال‌های عملی یاد می‌گیریم چگونه کدهایی انعطاف‌پذیر، قابل استفاده مجدد و دارای Type Safety بنویسیم.

Generics در TypeScript چیست؟

Generics قابلیتی در TypeScript است که به ما اجازه می‌دهد توابع، کلاس‌ها، Interfaceها و Type Aliasهایی ایجاد کنیم که بتوانند با انواع داده‌های مختلف کار کنند.

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

این قابلیت باعث می‌شود بدون از دست دادن بررسی نوع داده‌ها، از تکرار کد جلوگیری کنیم.

چرا به Generics نیاز داریم؟

فرض کنید دو تابع داریم که یکی مقدار عددی و دیگری مقدار رشته‌ای را دریافت کرده و برمی‌گرداند.

function getNumber(value: number): number {
  return value;
}

function getString(value: string): string {
  return value;
}

همان‌طور که مشاهده می‌کنید، منطق هر دو تابع یکسان است و فقط نوع داده متفاوت است.

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

با استفاده از Generics می‌توانیم این مشکل را برطرف کنیم.

تعریف اولین تابع Generic در TypeScript

برای تعریف یک تابع Generic از پارامتر نوع مانند T داخل علامت‌های زاویه‌ای استفاده می‌کنیم.

function getValue<T>(value: T): T {
  return value;
}

const name = getValue<string>("Milad");

const age = getValue<number>(25);

const isActive = getValue<boolean>(true);

در این مثال:

  • T یک پارامتر نوع یا Type Parameter است.
  • value: T مشخص می‌کند ورودی از نوع T باشد.
  • : T مشخص می‌کند خروجی نیز از همان نوع باشد.
  • هنگام فراخوانی تابع می‌توانیم نوع داده را تعیین کنیم.

حالا به جای ساخت چندین تابع، تنها یک تابع داریم که با انواع داده مختلف کار می‌کند.

تفاوت Generics و any در TypeScript

یکی از سوالات رایج هنگام یادگیری Generics این است که چرا به جای Generic از any استفاده نکنیم؟

در نگاه اول ممکن است هر دو روش مشابه به نظر برسند؛ اما تفاوت مهمی میان آن‌ها وجود دارد.

استفاده از any

function getData(value: any): any {
  return value;
}

const result = getData("Hello");

result.toFixed(2);

در این کد، TypeScript اجازه فراخوانی toFixed() را می‌دهد؛ زیرا نوع خروجی any است.

اما هنگام اجرا، چون مقدار واقعی یک رشته است، با خطا مواجه می‌شویم.

استفاده از Generic

function getData<T>(value: T): T {
  return value;
}

const result = getData("Hello");

result.toUpperCase();

// Error:
// result.toFixed(2);

در این حالت TypeScript نوع داده را حفظ می‌کند و استفاده نادرست از متد toFixed() را هنگام بررسی کد تشخیص می‌دهد.

بنابراین مهم‌ترین مزیت Generics نسبت به any، حفظ اطلاعات نوع داده و قابلیت بررسی خطاها است.

Type Inference در Generics

یکی دیگر از امکانات TypeScript، تشخیص خودکار نوع داده یا Type Inference است.

در بسیاری از مواقع نیازی نیست نوع Generic را به‌صورت دستی مشخص کنیم.

function identity<T>(value: T): T {
  return value;
}

const username = identity("Milad");

const score = identity(100);

const active = identity(true);

در این مثال TypeScript با توجه به ورودی‌ها، نوع مناسب را تشخیص می‌دهد.

این ویژگی باعث می‌شود کد کوتاه‌تر و خواناتر نوشته شود.

استفاده از Generics در آرایه‌ها

یکی از کاربردهای مهم Generics، کار با آرایه‌هایی است که می‌توانند شامل انواع داده متفاوت باشند.

برای مثال، تابعی می‌نویسیم که اولین عنصر یک آرایه را برگرداند.

function getFirstItem<T>(
  items: T[]
): T | undefined {
  return items[0];
}

const numbers = [10, 20, 30];

const names = ["Ali", "Milad", "Sara"];

const firstNumber = getFirstItem(numbers);

const firstName = getFirstItem(names);

در این کد:

  • نوع T با توجه به آرایه ورودی تعیین می‌شود.
  • خروجی تابع از نوع عنصر آرایه است.
  • اگر آرایه خالی باشد، مقدار undefined برگردانده می‌شود.

استفاده از T | undefined باعث می‌شود نوع خروجی، احتمال خالی بودن آرایه را نیز در نظر بگیرد.

تعریف Generic Interface در TypeScript

Generics فقط مخصوص توابع نیست و می‌توانیم از آن در Interfaceها نیز استفاده کنیم.

فرض کنید قصد داریم پاسخ‌های API را در یک پروژه مدیریت کنیم.

معمولاً پاسخ‌های API شامل اطلاعاتی مانند وضعیت درخواست، پیام و داده اصلی هستند.

می‌توانیم ساختاری عمومی ایجاد کنیم:

interface ApiResponse<T> {
  success: boolean;
  message: string;
  data: T;
}

حالا این Interface را برای اطلاعات کاربران استفاده می‌کنیم.

interface User {
  id: number;
  name: string;
  email: string;
}

const response: ApiResponse<User> = {
  success: true,
  message: "User loaded",
  data: {
    id: 1,
    name: "Milad",
    email: "milad@example.com"
  }
};

اگر API به جای یک کاربر، لیستی از کاربران را برگرداند، می‌توانیم به شکل زیر عمل کنیم:

const usersResponse: ApiResponse<User[]> = {
  success: true,
  message: "Users loaded",
  data: [
    {
      id: 1,
      name: "Milad",
      email: "milad@example.com"
    },
    {
      id: 2,
      name: "Ali",
      email: "ali@example.com"
    }
  ]
};

همان Interface را می‌توان برای محصولات، سفارش‌ها و سایر داده‌های پروژه نیز استفاده کرد.

این روش در پروژه‌های واقعی باعث کاهش کدهای تکراری و افزایش قابلیت نگهداری می‌شود.

نکته مهم: Generic Interface ساختار داده را هنگام بررسی TypeScript مشخص می‌کند؛ اما به‌تنهایی محتوای واقعی پاسخ API را در زمان اجرا اعتبارسنجی نمی‌کند.

Generic Constraints در TypeScript چیست؟

در برخی شرایط نمی‌خواهیم تابع Generic تمام انواع داده را بپذیرد.

برای مثال، ممکن است تابعی داشته باشیم که فقط با داده‌های دارای ویژگی length کار کند.

در این حالت از Generic Constraints استفاده می‌کنیم.

function getLength<T extends { length: number }>(
  value: T
): number {
  return value.length;
}

getLength("Hello");

getLength([1, 2, 3]);

getLength({ length: 10 });

در این مثال، عبارت extends محدودیتی برای نوع T ایجاد می‌کند.

یعنی هر مقداری که به تابع ارسال می‌شود، باید ویژگی length از نوع number داشته باشد.

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

// TypeScript Error
getLength(100);

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

استفاده از keyof همراه با Generics

یکی دیگر از کاربردهای مهم Generics، ترکیب آن با عملگر keyof است.

این ترکیب به ما کمک می‌کند فقط کلیدهای معتبر یک شیء را دریافت کنیم.

function getProperty<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const user = {
  id: 1,
  name: "Milad",
  age: 25
};

const name = getProperty(user, "name");

const age = getProperty(user, "age");

در این مثال:

  • T نوع شیء را مشخص می‌کند.
  • K یکی از کلیدهای معتبر آن شیء است.
  • keyof T مجموعه کلیدهای نوع T را به دست می‌آورد.
  • T[K] نوع مقدار مربوط به کلید انتخاب‌شده را مشخص می‌کند.

اگر کلیدی ارسال کنیم که در شیء وجود ندارد، TypeScript خطا نمایش می‌دهد.

// TypeScript Error
getProperty(user, "address");

این روش برای نوشتن توابع عمومی و Type-Safe در پروژه‌های حرفه‌ای بسیار کاربردی است.

استفاده از چند Generic Type Parameter

در TypeScript می‌توانیم به جای یک پارامتر نوع، چندین پارامتر تعریف کنیم.

برای مثال، تابعی ایجاد می‌کنیم که دو مقدار با انواع متفاوت دریافت کند.

function createPair<T, U>(
  first: T,
  second: U
): [T, U] {
  return [first, second];
}

const pair1 = createPair("Milad", 25);

const pair2 = createPair(true, "Active");

const pair3 = createPair(100, false);

در اینجا دو پارامتر T و U مستقل از یکدیگر هستند.

خروجی تابع یک Tuple است که نوع هر عنصر آن با ورودی متناظر مشخص می‌شود.

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

استفاده از Generics در Type Alias

علاوه بر Interface، می‌توان از Generics برای ایجاد Type Aliasهای انعطاف‌پذیر نیز استفاده کرد.

type Result<T> = {
  data: T;
  error: string | null;
};

const productResult: Result<string[]> = {
  data: ["Laptop", "Mouse", "Keyboard"],
  error: null
};

در این مثال Result<T> یک نوع عمومی است که می‌تواند برای انواع مختلف داده استفاده شود.

کاربرد Generics در پروژه‌های واقعی

Generics فقط یک قابلیت آموزشی نیست و در توسعه نرم‌افزارهای واقعی کاربردهای فراوانی دارد.

برخی از مهم‌ترین کاربردهای آن عبارت‌اند از:

  • مدیریت API: تعریف ساختار عمومی برای پاسخ‌های سرور
  • React: مشخص کردن نوع State و برخی Hookها و کامپوننت‌های عمومی
  • Angular: تعیین نوع داده در Observableها و سرویس‌ها
  • مدیریت آرایه‌ها: ساخت توابع عمومی برای پردازش مجموعه داده‌ها
  • مدیریت فرم‌ها: تعریف ساختارهای Type-Safe برای مقادیر فرم
  • توسعه کتابخانه‌ها: ایجاد ابزارهای قابل استفاده مجدد برای انواع مختلف داده

استفاده اصولی از Generics می‌تواند خوانایی کد، قابلیت استفاده مجدد و اطمینان از صحت نوع داده‌ها را افزایش دهد.

اشتباهات رایج هنگام استفاده از Generics

هنگام کار با Genericها بهتر است چند نکته را رعایت کنیم.

۱. استفاده بیش از حد از Generics

هر تابعی نیاز به Generic ندارد.

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

۲. استفاده از any به جای Generic

استفاده بی‌دلیل از any باعث از دست رفتن مزایای Type Safety می‌شود.

در شرایطی که لازم است رابطه میان نوع ورودی و خروجی حفظ شود، Generic معمولاً انتخاب مناسب‌تری است.

۳. نادیده گرفتن Generic Constraints

گاهی یک تابع به ویژگی خاصی از ورودی نیاز دارد.

در چنین شرایطی بهتر است محدودیت نوع مناسب تعریف کنیم تا TypeScript بتواند استفاده نادرست را تشخیص دهد.

۴. تصور اینکه Generics داده‌ها را در زمان اجرا بررسی می‌کند

Genericها بخشی از سیستم نوع TypeScript هستند و اطلاعات نوعی آن‌ها هنگام تبدیل کد به JavaScript حذف می‌شود.

برای اعتبارسنجی داده‌های دریافتی از API یا ورودی کاربران، باید از بررسی‌های زمان اجرا نیز استفاده کنیم.

سوالات متداول درباره Generics در TypeScript

Generics در TypeScript چیست؟

Generics قابلیتی است که به برنامه‌نویسان اجازه می‌دهد توابع، Interfaceها، Type Aliasها و کلاس‌هایی بنویسند که با انواع داده مختلف کار می‌کنند و اطلاعات نوع داده را حفظ می‌کنند.

تفاوت Generic و any چیست؟

نوع any بخش زیادی از بررسی نوع داده را غیرفعال می‌کند؛ اما Generic می‌تواند نوع ورودی و خروجی را به یکدیگر مرتبط کند و امکان تشخیص خطاهای نوعی را حفظ کند.

آیا Generics فقط در توابع استفاده می‌شود؟

خیر. Generics در توابع، Interfaceها، کلاس‌ها و Type Aliasها قابل استفاده است.

کاربرد Generic Constraints چیست؟

Generic Constraints برای محدود کردن انواع داده قابل قبول در یک Generic استفاده می‌شود. با استفاده از extends می‌توان مشخص کرد نوع ورودی باید دارای چه ساختاری باشد.

آیا یادگیری Generics برای React و Angular ضروری است؟

برای شروع کار با این فریمورک‌ها لازم نیست تمام قابلیت‌های پیشرفته Generics را بدانید؛ اما آشنایی با آن برای درک بهتر تایپ‌ها، Hookها، سرویس‌ها و ساختارهای قابل استفاده مجدد بسیار مفید است.

جمع‌بندی آموزش Generics در TypeScript

در این مقاله از جلسه دوازدهم آموزش TypeScript، با مفهوم Generics و نحوه استفاده از آن آشنا شدیم.

یاد گرفتیم چگونه با استفاده از Generic Functions، Generic Interfaces، Generic Constraints و عملگر keyof کدهایی منعطف‌تر و دارای Type Safety بنویسیم.

درک صحیح Generics یکی از گام‌های مهم برای ورود به مباحث پیشرفته TypeScript و توسعه پروژه‌های حرفه‌ای است.

اگر قصد دارید TypeScript را به‌صورت اصولی و پروژه‌محور یاد بگیرید، پیشنهاد می‌کنیم دوره کامل شده مشاهده کنین: