فارسی

راهنمای جامع کلمه‌ی کلیدی infer در تایپ‌اسکریپت برای استخراج و دستکاری قدرتمند تایپ‌ها با استفاده از تایپ‌های شرطی و بررسی موارد استفاده پیشرفته.

تسلط بر infer در تایپ‌اسکریپت: استخراج تایپ با تایپ‌های شرطی برای دستکاری پیشرفته تایپ‌ها

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

تایپ‌های شرطی (Conditional Types) چه هستند؟

قبل از اینکه به سراغ infer برویم، بیایید به سرعت تایپ‌های شرطی را مرور کنیم. تایپ‌های شرطی در تایپ‌اسکریپت به شما اجازه می‌دهند تا یک تایپ را بر اساس یک شرط تعریف کنید، مشابه عملگر سه‌تایی (ternary operator) در جاوااسکریپت. سینتکس اصلی آن به این صورت است:

T extends U ? X : Y

این به این صورت خوانده می‌شود: «اگر تایپ T قابل انتساب به تایپ U باشد، آنگاه تایپ برابر با X است؛ در غیر این صورت، تایپ برابر با Y است.»

مثال:

type IsString = T extends string ? true : false;

type StringResult = IsString; // type StringResult = true
type NumberResult = IsString; // type NumberResult = false

معرفی کلمه‌ی کلیدی infer

کلمه‌ی کلیدی infer در بخش extends یک تایپ شرطی برای تعریف یک متغیر تایپ استفاده می‌شود که می‌تواند از تایپ در حال بررسی استنتاج (infer) شود. در اصل، به شما اجازه می‌دهد تا بخشی از یک تایپ را برای استفاده‌های بعدی «ضبط» کنید.

سینتکس اصلی:

type MyType = T extends (infer U) ? U : never;

در این مثال، اگر T به تایپی قابل انتساب باشد، تایپ‌اسکریپت تلاش می‌کند تا تایپ U را استنتاج کند. اگر استنتاج موفقیت‌آمیز باشد، تایپ حاصل U خواهد بود؛ در غیر این صورت، never خواهد بود.

مثال‌های ساده از infer

۱. استنتاج تایپ خروجی یک تابع

یک مورد استفاده رایج، استنتاج تایپ خروجی یک تابع است:

type ReturnType any> = T extends (...args: any) => infer R ? R : any;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType; // type AddReturnType = number

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetReturnType = ReturnType; // type GreetReturnType = string

در این مثال، ReturnType یک تایپ تابع T را به عنوان ورودی می‌گیرد. این تایپ بررسی می‌کند که آیا T به یک تابع که هر آرگومانی را می‌پذیرد و مقداری را برمی‌گرداند قابل انتساب است یا خیر. اگر چنین باشد، تایپ خروجی را به عنوان R استنتاج کرده و آن را برمی‌گرداند. در غیر این صورت، any را برمی‌گرداند.

۲. استنتاج تایپ عناصر آرایه

سناریوی مفید دیگر، استخراج تایپ عناصر از یک آرایه است:

type ArrayElementType = T extends (infer U)[] ? U : never;

type NumberArrayType = ArrayElementType; // type NumberArrayType = number
type StringArrayType = ArrayElementType; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType; // type NotAnArrayType = never

در اینجا، ArrayElementType بررسی می‌کند که آیا T یک تایپ آرایه است یا خیر. اگر باشد، تایپ عناصر را به عنوان U استنتاج کرده و آن را برمی‌گرداند. در غیر این صورت، never را برمی‌گرداند.

موارد استفاده پیشرفته از infer

۱. استنتاج پارامترهای یک سازنده (Constructor)

شما می‌توانید از infer برای استخراج تایپ‌های پارامترهای یک تابع سازنده استفاده کنید:

type ConstructorParameters any> = T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters; // type PersonConstructorParams = [string, number]

class Point {
    constructor(public x: number, public y: number) {}
}

type PointConstructorParams = ConstructorParameters; // type PointConstructorParams = [number, number]

در این مورد، ConstructorParameters یک تایپ تابع سازنده T را می‌گیرد. این تایپ، تایپ‌های پارامترهای سازنده را به عنوان P استنتاج کرده و آنها را به صورت یک تاپل (tuple) برمی‌گرداند.

۲. استخراج پراپرتی‌ها از تایپ‌های آبجکت

infer همچنین می‌تواند برای استخراج پراپرتی‌های خاص از تایپ‌های آبجکت با استفاده از تایپ‌های نگاشتی (mapped types) و تایپ‌های شرطی استفاده شود:

type PickByType = {
  [P in K as T[P] extends U ? P : never]: T[P];
};

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  isActive: boolean;
}

type StringProperties = PickByType; // type StringProperties = { name: string; email: string; }

type NumberProperties = PickByType; // type NumberProperties = { id: number; age: number; }

// یک اینترفیس برای نمایش مختصات جغرافیایی.
interface GeoCoordinates {
    latitude: number;
    longitude: number;
    altitude: number;
    country: string;
    city: string;
    timezone: string;
}

type NumberCoordinateProperties = PickByType; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }

در اینجا، PickByType یک تایپ جدید ایجاد می‌کند که فقط شامل پراپرتی‌های T (با کلیدهایی در K) است که مقادیر آنها به تایپ U قابل انتساب هستند. تایپ نگاشتی روی کلیدهای T پیمایش می‌کند و تایپ شرطی کلیدهایی را که با تایپ مشخص شده مطابقت ندارند، فیلتر می‌کند.

۳. کار با Promiseها

شما می‌توانید تایپ resolved شده یک Promise را استنتاج کنید:

type Awaited = T extends Promise ? U : T;

async function fetchData(): Promise {
  return 'Data from API';
}

type FetchDataType = Awaited>; // type FetchDataType = string

async function fetchNumbers(): Promise {
    return [1, 2, 3];
}

type FetchedNumbersType = Awaited>; //type FetchedNumbersType = number[]

تایپ Awaited یک تایپ T را می‌گیرد که انتظار می‌رود یک Promise باشد. سپس این تایپ، تایپ resolved شده U از Promise را استنتاج کرده و آن را برمی‌گرداند. اگر T یک promise نباشد، خود T را برمی‌گرداند. این یک تایپ کمکی داخلی (built-in utility type) در نسخه‌های جدیدتر تایپ‌اسکریپت است.

۴. استخراج تایپ از آرایه‌ای از Promiseها

ترکیب Awaited و استنتاج تایپ آرایه به شما اجازه می‌دهد تا تایپ resolved شده توسط آرایه‌ای از Promiseها را استنتاج کنید. این امر به ویژه هنگام کار با Promise.all مفید است.

type PromiseArrayReturnType[]> = {
    [K in keyof T]: Awaited;
};


async function getUSDRate(): Promise {
  return 0.0069;
}

async function getEURRate(): Promise {
  return 0.0064;
}

const rates = [getUSDRate(), getEURRate()];

type RatesType = PromiseArrayReturnType;
// type RatesType = [number, number]

این مثال ابتدا دو تابع ناهمزمان getUSDRate و getEURRate را تعریف می‌کند که دریافت نرخ ارز را شبیه‌سازی می‌کنند. سپس تایپ کمکی PromiseArrayReturnType تایپ resolved شده از هر Promise در آرایه را استخراج می‌کند، که منجر به یک تایپ تاپل می‌شود که هر عنصر آن، تایپ await شده‌ی Promise مربوطه است.

مثال‌های عملی در حوزه‌های مختلف

۱. اپلیکیشن تجارت الکترونیک

یک اپلیکیشن تجارت الکترونیک را در نظر بگیرید که در آن جزئیات محصول را از یک API دریافت می‌کنید. می‌توانید از infer برای استخراج تایپ داده‌های محصول استفاده کنید:

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  category: string;
  rating: number;
  countryOfOrigin: string;
}

async function fetchProduct(productId: number): Promise {
  // شبیه‌سازی فراخوانی API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: productId,
        name: 'Example Product',
        price: 29.99,
        description: 'A sample product',
        imageUrl: 'https://example.com/image.jpg',
        category: 'Electronics',
        rating: 4.5,
        countryOfOrigin: 'Canada'
      });
    }, 500);
  });
}


type ProductType = Awaited>; // type ProductType = Product

function displayProductDetails(product: ProductType) {
  console.log(`Product Name: ${product.name}`);
  console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}

fetchProduct(123).then(displayProductDetails);

در این مثال، ما یک اینترفیس Product و یک تابع fetchProduct تعریف می‌کنیم که جزئیات محصول را از یک API دریافت می‌کند. ما از Awaited و ReturnType برای استخراج تایپ Product از تایپ خروجی تابع fetchProduct استفاده می‌کنیم، که به ما امکان می‌دهد تا تایپ تابع displayProductDetails را بررسی کنیم.

۲. بین‌المللی‌سازی (i18n)

فرض کنید یک تابع ترجمه دارید که رشته‌های مختلفی را بر اساس زبان محلی (locale) برمی‌گرداند. می‌توانید از infer برای استخراج تایپ خروجی این تابع برای ایمنی تایپ (type safety) استفاده کنید:

interface Translations {
  greeting: string;
  farewell: string;
  welcomeMessage: (name: string) => string;
}

const enTranslations: Translations = {
  greeting: 'Hello',
  farewell: 'Goodbye',
  welcomeMessage: (name: string) => `Welcome, ${name}!`, 
};

const frTranslations: Translations = {
  greeting: 'Bonjour',
  farewell: 'Au revoir',
  welcomeMessage: (name: string) => `Bienvenue, ${name}!`, 
};

function getTranslation(locale: 'en' | 'fr'): Translations {
  return locale === 'en' ? enTranslations : frTranslations;
}

type TranslationType = ReturnType;

function greetUser(locale: 'en' | 'fr', name: string) {
  const translations = getTranslation(locale);
  console.log(translations.welcomeMessage(name));
}

greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!

در اینجا، TranslationType به عنوان اینترفیس Translations استنتاج می‌شود، که تضمین می‌کند تابع greetUser اطلاعات تایپ صحیحی برای دسترسی به رشته‌های ترجمه شده دارد.

۳. مدیریت پاسخ API

هنگام کار با APIها، ساختار پاسخ می‌تواند پیچیده باشد. infer می‌تواند به استخراج تایپ‌های داده خاص از پاسخ‌های API تودرتو کمک کند:

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

interface UserData {
  id: number;
  username: string;
  email: string;
  profile: {
    firstName: string;
    lastName: string;
    country: string;
    language: string;
  }
}

async function fetchUser(userId: number): Promise> {
  // شبیه‌سازی فراخوانی API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        status: 200,
        data: {
          id: userId,
          username: 'johndoe',
          email: 'john.doe@example.com',
          profile: {
            firstName: 'John',
            lastName: 'Doe',
            country: 'USA',
            language: 'en'
          }
        }
      });
    }, 500);
  });
}


type UserApiResponse = Awaited>;

type UserProfileType = UserApiResponse['data']['profile'];

function displayUserProfile(profile: UserProfileType) {
  console.log(`Name: ${profile.firstName} ${profile.lastName}`);
  console.log(`Country: ${profile.country}`);
}

fetchUser(123).then((response) => {
  if (response.status === 200) {
    displayUserProfile(response.data.profile);
  }
});

در این مثال، ما یک اینترفیس ApiResponse و یک اینترفیس UserData تعریف می‌کنیم. ما از infer و نمایه‌سازی تایپ (type indexing) برای استخراج UserProfileType از پاسخ API استفاده می‌کنیم، که تضمین می‌کند تابع displayUserProfile تایپ صحیحی را دریافت می‌کند.

بهترین شیوه‌ها برای استفاده از infer

اشتباهات رایج

جایگزین‌های infer

در حالی که infer ابزار قدرتمندی است، شرایطی وجود دارد که رویکردهای جایگزین ممکن است مناسب‌تر باشند:

نتیجه‌گیری

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