מדריך מקיף למילת המפתח 'infer' ב-TypeScript, המסביר כיצד להשתמש בה עם טיפוסים מותנים לחילוץ ומניפולציה עוצמתיים של טיפוסים, כולל מקרי שימוש מתקדמים.
שליטה ב-TypeScript Infer: חילוץ טיפוסים מותנה למניפולציה מתקדמת של טיפוסים
מערכת הטיפוסים של 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
גם כדי לחלץ מאפיינים ספציפיים מטיפוסי אובייקט באמצעות טיפוסים ממופים וטיפוסים מותנים:
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
. הטיפוס הממופה עובר על המפתחות של T
, והטיפוס המותנה מסנן את המפתחות שאינם תואמים לטיפוס שצוין.
3. עבודה עם Promises
ניתן להסיק את הטיפוס המתקבל (resolved) מ-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 שבו כל איבר הוא הטיפוס הממתין (awaited) של ה-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
השולפת פרטי מוצר מ-API. אנו משתמשים ב-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
בעבודה עם ממשקי API, מבנה התגובה יכול להיות מורכב. 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 עשויה להסיק טיפוס שאינו מה שציפיתם. בדקו שוב את הגדרות הטיפוסים והתנאים שלכם.
- תלויות מעגליות: היזהרו בעת הגדרת טיפוסים רקורסיביים באמצעות
infer
, מכיוון שהם עלולים להוביל לתלויות מעגליות ולשגיאות קומפילציה. - טיפוסים מורכבים מדי: הימנעו מיצירת טיפוסים מותנים מורכבים מדי שקשה להבין ולתחזק. פרקו אותם לטיפוסים קטנים יותר וקלים לניהול.
חלופות ל-infer
בעוד ש-infer
הוא כלי רב עוצמה, ישנם מצבים שבהם גישות חלופיות עשויות להיות מתאימות יותר:
- הצהרות טיפוס (Type Assertions): במקרים מסוימים, ניתן להשתמש בהצהרות טיפוס כדי לציין במפורש את הטיפוס של ערך במקום להסיק אותו. עם זאת, היזהרו עם הצהרות טיפוס, מכיוון שהן יכולות לעקוף את בדיקת הטיפוסים.
- שומרי טיפוס (Type Guards): ניתן להשתמש בשומרי טיפוס כדי לצמצם את הטיפוס של ערך על סמך בדיקות בזמן ריצה. זה שימושי כאשר אתם צריכים לטפל בטיפוסים שונים על סמך תנאים בזמן ריצה.
- טיפוסי עזר: TypeScript מספקת סט עשיר של טיפוסי עזר שיכולים לטפל במשימות מניפולציה נפוצות רבות של טיפוסים ללא צורך בהצהרות
infer
מותאמות אישית.
סיכום
מילת המפתח infer
ב-TypeScript, בשילוב עם טיפוסים מותנים, פותחת יכולות מתקדמות של מניפולציית טיפוסים. היא מאפשרת לכם לחלץ טיפוסים ספציפיים ממבני טיפוסים מורכבים, ובכך מאפשרת לכם לכתוב קוד חזק יותר, קל לתחזוקה ובטוח מבחינת טיפוסים. מהסקת טיפוסי החזרה של פונקציות ועד לחילוץ מאפיינים מטיפוסי אובייקט, האפשרויות הן רבות. על ידי הבנת העקרונות והשיטות המומלצות המתוארות במדריך זה, תוכלו למנף את infer
במלוא הפוטנציאל שלה ולשדרג את כישורי ה-TypeScript שלכם. זכרו לתעד את הטיפוסים שלכם, לבדוק אותם ביסודיות ולשקול גישות חלופיות בעת הצורך. שליטה ב-infer
מעצימה אתכם לכתוב קוד TypeScript אקספרסיבי ועוצמתי באמת, שמוביל בסופו של דבר לתוכנה טובה יותר.