Frontend Development

تسلط بر ژنریک‌های TypeScript برای ایجاد پوشش‌های کلاینت API قوی در برنامه‌های سازمانی

با افزایش پیچیدگی برنامه‌های فرانت‌اند، شکاف بین منطق سمت کلاینت و ساختارهای داده سمت سرور گسترش می‌یابد. در محیط‌های سازمانی بزرگ، تکیه بر پاسخ‌های JSON با تایپ‌بندی ضعیف، راهی مطمئن برای بروز خطاهای زمان اجراست که عیب‌یابی آن‌ها دشوار است. راه حل در یک ویژگی قدرتمند TypeScript نهفته است: ژنریک‌ها (Generics). با پیاده‌سازی یک پوشش کلاینت API مبتنی بر ژنریک، توسعه‌دهندگان می‌توانند ایمنی نوع (Type Safety) سخت‌گیرانه را در تمام لایه‌ها اعمال کنند و اطمینان حاصل کنند که داده‌های دریافتی از سرور کاملاً با کامپوننت‌های مصرف‌کننده همسو است. این مقاله به بررسی نحوه معماری یک کلاینت API مقیاس‌پذیر و دارای تایپ ایمن با استفاده از الگوهای پیشرفته TypeScript می‌پردازد.

خطرات کلاینت‌های API غیرژنریک

یک پیاده‌سازی ساده و معمولی از کلاینت API را در نظر بگیرید. این کلاینت معمولاً یک URL را می‌پذیرد و یک `Promise` یا یک `Promise` عمومی را برمی‌گرداند. اگرچه این روش برای نمونه‌سازی اولیه کار می‌کند، اما در هنگام بازنویسی کد (Refactoring) یا با تغییرات در طرحواره (Schema) سمت سرور، هیچ حفاظتی ارائه نمی‌دهد. اگر سمت سرور یک عدد صحیح را به رشته تغییر دهد، برنامه شما بدون خطا کامپایل می‌شود اما در زمان اجرا دچار خطا می‌شود. این حالت «شکست خاموش» دشمن قابلیت نگهداری است.

برای مقابله با این مشکل، به الگویی نیاز داریم که اطلاعات نوع را از مدل‌های سمت سرور تا کامپوننت‌های رابط کاربری (UI) گسترش دهد. اینجا است که TypeScript Generics درخشش می‌کنند و به ما اجازه می‌دهند یک نقشه انعطاف‌پذیر تعریف کنیم که هنگام استفاده، آرگومان‌های نوع خاصی را می‌پذیرد.

معماری پوشش ژنریک

هسته راه حل ما یک `httpClient` است که توسط نوع پاسخ مورد انتظار `T` پارامتربندی شده است. به جای سفت و سخت کردن نوع بازگشتی، کلاس را به گونه‌ای تعریف می‌کنیم که خنثی باشد و منتظر بماند تا فراخواننده مشخص کند چه ساختار داده‌ای را انتظار دارد. این رویکرد تضمین می‌کند که هر متد API دقیقاً همان نوعی را که تعریف شده است، برمی‌گرداند و از IntelliSense و اعتبارسنجی در زمان کامپایل پشتیبانی می‌کند.

interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

class ApiClient<TResponse> {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<TData = TResponse>(endpoint: string): Promise<ApiResponse<TData>> {
    const response = await fetch(`${this.baseUrl}/${endpoint}`);
    const json = await response.json();
    
    // در یک برنامه واقعی، ممکن است بخواهید شکل داده را اینجا اعتبارسنجی کنید
    return {
      data: json,
      status: response.status,
    };
  }

  async post<TData = TResponse>(endpoint: string, body: unknown): Promise<ApiResponse<TData>> {
    const response = await fetch(`${this.baseUrl}/${endpoint}`, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: { 'Content-Type': 'application/json' }
    });
    
    const json = await response.json();
    return {
      data: json,
      status: response.status,
    };
  }
}

پیاده‌سازی بالا از پارامتر نوع ژنریک `` برای کلاس استفاده می‌کند. با این حال، در بسیاری از سناریوها، نوع بازگشتی یک endpoint خاص ممکن است با پیکربندی پایه کلاس متفاوت باشد. بنابراین، ما به متدهای فردی اجازه می‌دهیم در صورت نیاز پارامتر ژنریک را با `` جایگزین کنند که انعطاف‌پذیری حداکثری را فراهم می‌کند.

تعریف مدل‌های داده با تایپ سخت‌گیرانه

یک کلاینت ژنریک به اندازه‌ای قوی است که با انواع داده‌ای که با آن Instantiated (نهادینه) می‌شود. در یک برنامه بزرگ، باید رابط‌های صریحی برای شکل‌های داده‌ای خود تعریف کنیم. این کار تضمین می‌کند که پوشش کلاینت دقیقاً بداند چه انتظاری داشته باشد.

interface User {
  id: string;
  email: string;
  profile: {
    firstName: string;
    lastName: string;
  };
}

interface Post {
  postId: number;
  title: string;
  content: string;
}

// نهادینه‌سازی کلاینت‌ها با انواع خاص
const userClient = new ApiClient<User>('https://api.example.com');
const postClient = new ApiClient<Post>('https://api.example.com');

با ارسال `User` و `Post` به عنوان آرگومان‌های ژنریک به `ApiClient`، نوع بازگشتی متدهای `get` و `post` را قفل می‌کنیم. اگر بعداً تلاش کنیم به ویژگی‌ای دسترسی پیدا کنیم که در رابط `User` وجود ندارد، کامپایلر TypeScript بلافاصله خطا را نشان می‌دهد. این تغییر از خطاهای زمان اجرا به هشدارهای زمان کامپایل، سنگ بنای قابلیت اطمینان در فرانت‌اند مدرن است.

کاربرد پیشرفته: مدیریت پاسخ‌های پیچیده

APIهای دنیای واقعی اغلب نتایج صفحه‌بندی شده یا اشیاء تو در تو پیچیده‌ای را برمی‌گردانند که به خوبی در یک رابط واحد نمی‌گنجند. ژنریک‌ها به ما اجازه می‌دهند متدهای پوشش خاص برای این سناریوها ایجاد کنیم بدون اینکه ایمنی نوع کلاینت اصلی را خدشه‌دار کنیم.

برای مثال، یک پاسخ صفحه‌بندی ممکن است به شکل `{ data: User[]; page: number; total: number; }` باشد. می‌توانیم یک رابط پاسخ تخصصی ایجاد کنیم و از یک متد ژنریک برای مدیریت آن استفاده کنیم تا هم لیست کاربران و هم متادیتای صفحه‌بندی به طور قوی تایپ شوند.

interface PaginatedResponse<T> {
  items: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
}

// استفاده
async function getUsers(): Promise<PaginatedResponse<User>> {
  const response = await userClient.get<PaginatedResponse<User>>('users');
  return response.data;
}

این الگو تضمین می‌کند که فراخواننده `getUsers` یک آرایه از اشیاء `User` را دریافت می‌کند، نه صرفاً یک آرایه دلخواه، و می‌تواند با پشتیبانی کامل IDE به ویژگی‌های `page` و `total` با اطمینان دسترسی داشته باشد.

نتیجه‌گیری

پیاده‌سازی ژنریک‌های TypeScript برای پوشش‌های کلاینت API، صرفاً یک تمرین نحوی نیست؛ بلکه یک تصمیم معماری استراتژیک است. این کار کدبیس فرانت‌اند شما را به یک سیستم خودمستند و خوداعتبارسنج تبدیل می‌کند که در آن کامپایلر به عنوان خط مقدم دفاع در برابر ناسازگاری‌های داده عمل می‌کند. با اتخاذ این الگو، تیم‌ها می‌توانند با اطمینان طرحواره‌های سمت سرور را بازنویسی کنند، با این اطمینان که هرگونه ناسازگاری قبل از اینکه کد به محیط تولید برسد، شناسایی خواهد شد. برای توسعه‌دهندگان متوسط تا پیشرفته، تسلط بر این الگو برای ساخت برنامه‌های بزرگ‌مقیاس و قوی که در برابر آزمون زمان دوام می‌آورند، ضروری است.

Share:

Technical Tutorials — Powered by MiCms