العربية

دليل شامل لكلمة 'infer' في TypeScript، يشرح كيفية استخدامها مع الأنواع الشرطية لاستخلاص الأنواع ومعالجتها بفعالية، ويتضمن حالات استخدام متقدمة.

إتقان infer في TypeScript: استخلاص الأنواع الشرطي لمعالجة الأنواع المتقدمة

نظام الأنواع في TypeScript قوي بشكل لا يصدق، مما يسمح للمطورين بإنشاء تطبيقات قوية وقابلة للصيانة. إحدى الميزات الرئيسية التي تتيح هذه القوة هي الكلمة المفتاحية infer المستخدمة بالاقتران مع الأنواع الشرطية. يوفر هذا المزيج آلية لاستخلاص أنواع معينة من هياكل الأنواع المعقدة. تتعمق هذه المقالة في الكلمة المفتاحية infer، حيث تشرح وظيفتها وتعرض حالات استخدام متقدمة. سنستكشف أمثلة عملية قابلة للتطبيق على سيناريوهات تطوير برمجيات متنوعة، من التفاعل مع واجهات برمجة التطبيقات (API) إلى معالجة هياكل البيانات المعقدة.

ما هي الأنواع الشرطية؟

قبل أن نتعمق في infer، دعنا نراجع بسرعة الأنواع الشرطية. تسمح لك الأنواع الشرطية في TypeScript بتعريف نوع بناءً على شرط، على غرار المعامل الثلاثي في JavaScript. الصيغة الأساسية هي:

T extends U ? X : Y

يُقرأ هذا كالتالي: "إذا كان النوع T قابلاً للإسناد إلى النوع U، فإن النوع يكون X؛ وإلا، فإن النوع يكون Y."

مثال:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // النوع StringResult = true
type NumberResult = IsString<number>; // النوع NumberResult = false

تقديم الكلمة المفتاحية infer

تُستخدم الكلمة المفتاحية infer داخل جملة extends لنوع شرطي للإعلان عن متغير نوع يمكن استنتاجه من النوع الذي يتم التحقق منه. في جوهرها، تسمح لك "بالتقاط" جزء من النوع لاستخدامه لاحقًا.

الصيغة الأساسية:

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

في هذا المثال، إذا كان T قابلاً للإسناد إلى نوع ما، فسيحاول TypeScript استنتاج نوع U. إذا نجح الاستنتاج، فسيكون النوع هو U؛ وإلا، فسيكون never.

أمثلة بسيطة على infer

1. استنتاج نوع القيمة المُرجعة من دالة

أحد حالات الاستخدام الشائعة هو استنتاج نوع القيمة المُرجعة من دالة:

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

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

type AddReturnType = ReturnType<typeof add>; // النوع AddReturnType = number

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

type GreetReturnType = ReturnType<typeof greet>; // النوع GreetReturnType = string

في هذا المثال، يأخذ ReturnType<T> نوع دالة T كمدخل. يتحقق مما إذا كان T قابلاً للإسناد إلى دالة تقبل أي وسائط وتعيد قيمة. إذا كان الأمر كذلك، فإنه يستنتج نوع الإرجاع كـ R ويعيده. وإلا، فإنه يعيد any.

2. استنتاج نوع عنصر المصفوفة

سيناريو آخر مفيد هو استخلاص نوع العنصر من مصفوفة:

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

type NumberArrayType = ArrayElementType<number[]>; // النوع NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // النوع StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // النوع MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // النوع NotAnArrayType = never

هنا، يتحقق ArrayElementType<T> مما إذا كان T نوع مصفوفة. إذا كان كذلك، فإنه يستنتج نوع العنصر كـ U ويعيده. إذا لم يكن كذلك، فإنه يعيد never.

حالات استخدام متقدمة لـ infer

1. استنتاج معاملات المُنشئ (Constructor)

يمكنك استخدام infer لاستخلاص أنواع معاملات دالة المُنشئ:

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

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

type PersonConstructorParams = ConstructorParameters<typeof Person>; // النوع PersonConstructorParams = [string, number]

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

type PointConstructorParams = ConstructorParameters<typeof Point>; // النوع PointConstructorParams = [number, number]

في هذه الحالة، يأخذ ConstructorParameters<T> نوع دالة مُنشئ T. يستنتج أنواع معاملات المُنشئ كـ P ويعيدها كصف (tuple).

2. استخلاص الخصائص من أنواع الكائنات

يمكن أيضًا استخدام infer لاستخلاص خصائص معينة من أنواع الكائنات باستخدام الأنواع المعينة (mapped types) والأنواع الشرطية:

type PickByType<T, K extends keyof T, U> = {
  [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<User, keyof User, string>; // النوع StringProperties = { name: string; email: string; }

type NumberProperties = PickByType<User, keyof User, number>; // النوع NumberProperties = { id: number; age: number; }

// واجهة تمثل الإحداثيات الجغرافية.
interface GeoCoordinates {
    latitude: number;
    longitude: number;
    altitude: number;
    country: string;
    city: string;
    timezone: string;
}

type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // النوع NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }

هنا، يقوم PickByType<T, K, U> بإنشاء نوع جديد يتضمن فقط خصائص T (ذات المفاتيح الموجودة في K) التي تكون قيمها قابلة للإسناد إلى النوع U. يتكرر النوع المعين (mapped type) عبر مفاتيح T، ويقوم النوع الشرطي بتصفية المفاتيح التي لا تتطابق مع النوع المحدد.

3. التعامل مع Promises

يمكنك استنتاج النوع الذي يتم حله لـ Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;

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

type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // النوع FetchDataType = string

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

type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; // النوع FetchedNumbersType = number[]

يأخذ النوع Awaited<T> نوعًا T، والذي من المتوقع أن يكون Promise. ثم يستنتج النوع النوع الذي تم حله U من الـ Promise ويعيده. إذا لم يكن T من نوع Promise، فإنه يعيد T. هذا نوع مساعد مدمج في الإصدارات الأحدث من TypeScript.

4. استخلاص النوع من مصفوفة من Promises

يتيح لك الجمع بين Awaited واستنتاج نوع المصفوفة استنتاج النوع الذي تم حله بواسطة مصفوفة من Promises. هذا مفيد بشكل خاص عند التعامل مع Promise.all.

type PromiseArrayReturnType<T extends Promise<any>[]> = {
    [K in keyof T]: Awaited<T[K]>;
};


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

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

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

type RatesType = PromiseArrayReturnType<typeof rates>;
// النوع RatesType = [number, number]

يحدد هذا المثال أولاً دالتين غير متزامنتين، getUSDRate و getEURRate، تحاكيان جلب أسعار الصرف. ثم يستخلص النوع المساعد PromiseArrayReturnType النوع الذي تم حله من كل Promise في المصفوفة، مما ينتج عنه نوع صف (tuple) حيث يكون كل عنصر هو النوع المنتظر للـ Promise المقابل.

أمثلة عملية عبر مجالات مختلفة

1. تطبيق تجارة إلكترونية

فكر في تطبيق للتجارة الإلكترونية حيث تجلب تفاصيل المنتج من واجهة برمجة تطبيقات (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<Product> {
  // محاكاة استدعاء 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<ReturnType<typeof fetchProduct>>; // النوع 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 تجلب تفاصيل المنتج من واجهة برمجة تطبيقات. نستخدم Awaited و ReturnType لاستخلاص نوع Product من نوع القيمة المُرجعة لدالة fetchProduct، مما يسمح لنا بالتحقق من نوع دالة displayProductDetails.

2. التدويل (i18n)

لنفترض أن لديك دالة ترجمة تعيد سلاسل نصية مختلفة بناءً على الإعدادات المحلية (locale). يمكنك استخدام infer لاستخلاص نوع القيمة المُرجعة لهذه الدالة لضمان سلامة الأنواع:

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<typeof getTranslation>;

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

greetUser('fr', 'Jean'); // المخرجات: Bienvenue, Jean!

هنا، يتم استنتاج TranslationType ليكون واجهة Translations، مما يضمن أن دالة greetUser لديها معلومات النوع الصحيحة للوصول إلى السلاسل النصية المترجمة.

3. التعامل مع استجابات API

عند العمل مع واجهات برمجة التطبيقات (APIs)، يمكن أن يكون هيكل الاستجابة معقدًا. يمكن أن يساعد infer في استخلاص أنواع بيانات محددة من استجابات API المتداخلة:

interface ApiResponse<T> {
  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<ApiResponse<UserData>> {
  // محاكاة استدعاء 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<ReturnType<typeof fetchUser>>;

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 وفهرسة الأنواع لاستخلاص UserProfileType من استجابة API، مما يضمن أن دالة displayUserProfile تتلقى النوع الصحيح.

أفضل الممارسات لاستخدام infer

الأخطاء الشائعة

بدائل لـ infer

بينما تعتبر infer أداة قوية، هناك مواقف قد تكون فيها الأساليب البديلة أكثر ملاءمة:

الخاتمة

الكلمة المفتاحية infer في TypeScript، عند دمجها مع الأنواع الشرطية، تفتح إمكانيات متقدمة لمعالجة الأنواع. تسمح لك باستخلاص أنواع محددة من هياكل الأنواع المعقدة، مما يتيح لك كتابة شيفرة برمجية أكثر قوة وقابلية للصيانة وأمانًا من حيث الأنواع. من استنتاج أنواع الإرجاع للدوال إلى استخلاص الخصائص من أنواع الكائنات، فإن الاحتمالات واسعة. من خلال فهم المبادئ وأفضل الممارسات الموضحة في هذا الدليل، يمكنك الاستفادة من infer إلى أقصى إمكاناتها والارتقاء بمهاراتك في TypeScript. تذكر توثيق أنواعك واختبارها جيدًا والنظر في الأساليب البديلة عند الاقتضاء. إتقان infer يمكّنك من كتابة شيفرة TypeScript معبرة وقوية حقًا، مما يؤدي في النهاية إلى برمجيات أفضل.