دليل شامل لكلمة '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
. - اختبر أنواعك: استخدم مدقق الأنواع في TypeScript للتأكد من أن أنواعك تعمل كما هو متوقع.
- ضع الأداء في الاعتبار: يمكن أن تؤثر الأنواع الشرطية المعقدة أحيانًا على وقت التجميع. كن واعيًا بمدى تعقيد أنواعك.
- استخدم الأنواع المساعدة: يوفر TypeScript العديد من الأنواع المساعدة المدمجة (مثل
ReturnType
،Awaited
) التي يمكن أن تبسط الشيفرة البرمجية وتقلل من الحاجة إلى عباراتinfer
مخصصة.
الأخطاء الشائعة
- الاستنتاج غير الصحيح: في بعض الأحيان، قد يستنتج TypeScript نوعًا ليس كما تتوقع. تحقق مرة أخرى من تعريفات وأنواع الشروط الخاصة بك.
- التبعيات الدائرية: كن حذرًا عند تعريف الأنواع العودية (recursive) باستخدام
infer
، حيث يمكن أن تؤدي إلى تبعيات دائرية وأخطاء في التجميع. - الأنواع المفرطة في التعقيد: تجنب إنشاء أنواع شرطية معقدة للغاية يصعب فهمها وصيانتها. قم بتقسيمها إلى أنواع أصغر وأكثر قابلية للإدارة.
بدائل لـ infer
بينما تعتبر infer
أداة قوية، هناك مواقف قد تكون فيها الأساليب البديلة أكثر ملاءمة:
- تأكيدات النوع (Type Assertions): في بعض الحالات، يمكنك استخدام تأكيدات النوع لتحديد نوع القيمة بشكل صريح بدلاً من استنتاجه. ومع ذلك، كن حذرًا مع تأكيدات النوع، حيث يمكنها تجاوز التحقق من النوع.
- حراس النوع (Type Guards): يمكن استخدام حراس النوع لتضييق نطاق نوع القيمة بناءً على عمليات التحقق في وقت التشغيل. هذا مفيد عندما تحتاج إلى التعامل مع أنواع مختلفة بناءً على ظروف وقت التشغيل.
- الأنواع المساعدة (Utility Types): يوفر TypeScript مجموعة غنية من الأنواع المساعدة التي يمكنها التعامل مع العديد من مهام معالجة الأنواع الشائعة دون الحاجة إلى عبارات
infer
مخصصة.
الخاتمة
الكلمة المفتاحية infer
في TypeScript، عند دمجها مع الأنواع الشرطية، تفتح إمكانيات متقدمة لمعالجة الأنواع. تسمح لك باستخلاص أنواع محددة من هياكل الأنواع المعقدة، مما يتيح لك كتابة شيفرة برمجية أكثر قوة وقابلية للصيانة وأمانًا من حيث الأنواع. من استنتاج أنواع الإرجاع للدوال إلى استخلاص الخصائص من أنواع الكائنات، فإن الاحتمالات واسعة. من خلال فهم المبادئ وأفضل الممارسات الموضحة في هذا الدليل، يمكنك الاستفادة من infer
إلى أقصى إمكاناتها والارتقاء بمهاراتك في TypeScript. تذكر توثيق أنواعك واختبارها جيدًا والنظر في الأساليب البديلة عند الاقتضاء. إتقان infer
يمكّنك من كتابة شيفرة TypeScript معبرة وقوية حقًا، مما يؤدي في النهاية إلى برمجيات أفضل.