מדריך מקיף לטיפוסים ממופים וטיפוסים מותנים העוצמתיים של TypeScript, כולל דוגמאות מעשיות ומקרי שימוש מתקדמים ליצירת יישומים חזקים ובטוחים מבחינת טיפוסים.
שליטה בטיפוסים ממופים וטיפוסים מותנים של TypeScript
TypeScript, הרחבה של JavaScript, מציעה תכונות עוצמתיות ליצירת יישומים חזקים וניתנים לתחזוקה. בין התכונות הללו, טיפוסים ממופים (Mapped Types) וטיפוסים מותנים (Conditional Types) בולטים ככלים חיוניים למניפולציה מתקדמת של טיפוסים. מדריך זה מספק סקירה מקיפה של מושגים אלה, תוך בחינת התחביר שלהם, יישומים מעשיים ומקרי שימוש מתקדמים. בין אם אתם מפתחי TypeScript מנוסים או רק מתחילים את דרככם, מאמר זה יצייד אתכם בידע הדרוש לשימוש יעיל בתכונות אלו.
מהם טיפוסים ממופים?
טיפוסים ממופים מאפשרים ליצור טיפוסים חדשים על ידי טרנספורמציה של טיפוסים קיימים. הם עוברים על המאפיינים של טיפוס קיים ומחילים טרנספורמציה על כל מאפיין. זה שימושי במיוחד ליצירת וריאציות של טיפוסים קיימים, כמו הפיכת כל המאפיינים לאופציונליים או לקריאה-בלבד.
תחביר בסיסי
התחביר של טיפוס ממופה הוא כדלקמן:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: טיפוס הקלט שעליו רוצים למפות.K in keyof T
: עובר על כל מפתח בטיפוס הקלטT
.keyof T
יוצר איחוד (union) של כל שמות המאפיינים ב-T
, ו-K
מייצג כל מפתח בודד במהלך האיטרציה.Transformation
: הטרנספורמציה שרוצים להחיל על כל מאפיין. זה יכול להיות הוספת משנה תכונה (modifier) (כמוreadonly
או?
), שינוי הטיפוס, או משהו אחר לגמרי.
דוגמאות מעשיות
הפיכת מאפיינים לקריאה-בלבד
נניח שיש לכם ממשק המייצג פרופיל משתמש:
interface UserProfile {
name: string;
age: number;
email: string;
}
ניתן ליצור טיפוס חדש שבו כל המאפיינים הם לקריאה-בלבד:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
כעת, ל-ReadOnlyUserProfile
יהיו אותם מאפיינים כמו ל-UserProfile
, אך כולם יהיו לקריאה-בלבד.
הפיכת מאפיינים לאופציונליים
באופן דומה, ניתן להפוך את כל המאפיינים לאופציונליים:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
ל-OptionalUserProfile
יהיו כל המאפיינים של UserProfile
, אך כל מאפיין יהיה אופציונלי.
שינוי טיפוסי מאפיינים
אפשר גם לשנות את הטיפוס של כל מאפיין. לדוגמה, ניתן להפוך את כל המאפיינים למחרוזות (string):
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
במקרה זה, כל המאפיינים ב-StringifiedUserProfile
יהיו מטיפוס string
.
מהם טיפוסים מותנים?
טיפוסים מותנים מאפשרים להגדיр טיפוסים התלויים בתנאי. הם מספקים דרך לבטא יחסי טיפוסים המבוססים על האם טיפוס מסוים עומד באילוץ מסוים. זה דומה לאופרטור טרנרי ב-JavaScript, אבל עבור טיפוסים.
תחביר בסיסי
התחביר של טיפוס מותנה הוא כדלקמן:
T extends U ? X : Y
T
: הטיפוס הנבדק.U
: הטיפוס ש-T
מרחיב (התנאי).X
: הטיפוס שיוחזר אםT
מרחיב אתU
(התנאי מתקיים).Y
: הטיפוס שיוחזר אםT
אינו מרחיב אתU
(התנאי אינו מתקיים).
דוגמאות מעשיות
קביעה אם טיפוס הוא מחרוזת
ניצור טיפוס שמחזיר string
אם טיפוס הקלט הוא מחרוזת, ו-number
אחרת:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
חילוץ טיפוס מתוך איחוד (Union)
ניתן להשתמש בטיפוסים מותנים כדי לחלץ טיפוס ספציפי מתוך טיפוס איחוד. לדוגמה, כדי לחלץ טיפוסים שאינם null:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
כאן, אם T
הוא null
או undefined
, הטיפוס הופך ל-never
, אשר לאחר מכן מסונן החוצה על ידי פישוט טיפוסי האיחוד של TypeScript.
הסקת טיפוסים (Inferring Types)
ניתן להשתמש בטיפוסים מותנים גם כדי להסיק טיפוסים באמצעות מילת המפתח infer
. זה מאפשר לחלץ טיפוס ממבנה טיפוס מורכב יותר.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
בדוגמה זו, ReturnType
מחלץ את טיפוס ההחזרה של פונקציה. הוא בודק אם T
היא פונקציה שמקבלת ארגומנטים כלשהם ומחזירה טיפוס R
. אם כן, הוא מחזיר את R
; אחרת, הוא מחזיר any
.
שילוב טיפוסים ממופים וטיפוסים מותנים
הכוח האמיתי של טיפוסים ממופים וטיפוסים מותנים מגיע מהשילוב ביניהם. זה מאפשר ליצור טרנספורמציות טיפוסים גמישות ומלאות הבעה.
דוגמה: Deep Readonly
מקרה שימוש נפוץ הוא יצירת טיפוס שהופך את כל המאפיינים של אובייקט, כולל מאפיינים מקוננים, לקריאה-בלבד. ניתן להשיג זאת באמצעות טיפוס מותנה רקורסיבי.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
כאן, DeepReadonly
מחיל באופן רקורסיבי את משנה התכונה readonly
על כל המאפיינים והמאפיינים המקוננים שלהם. אם מאפיין הוא אובייקט, הוא קורא באופן רקורסיבי ל-DeepReadonly
על אותו אובייקט. אחרת, הוא פשוט מחיל את משנה התכונה readonly
על המאפיין.
דוגמה: סינון מאפיינים לפי טיפוס
נניח שברצונכם ליצור טיפוס שכולל רק מאפיינים מטיפוס ספציפי. ניתן לשלב טיפוסים ממופים וטיפוסים מותנים כדי להשיג זאת.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
בדוגמה זו, FilterByType
עובר על המאפיינים של T
ובודק אם הטיפוס של כל מאפיין מרחיב את U
. אם כן, הוא כולל את המאפיין בטיפוס התוצאה; אחרת, הוא מוציא אותו על ידי מיפוי המפתח ל-never
. שימו לב לשימוש ב-"as" למיפוי מחדש של מפתחות. לאחר מכן אנו משתמשים ב-`Omit` וב-`keyof StringProperties` כדי להסיר את מאפייני המחרוזת מהממשק המקורי.
מקרי שימוש ודפוסים מתקדמים
מעבר לדוגמאות הבסיסיות, ניתן להשתמש בטיפוסים ממופים וטיפוסים מותנים בתרחישים מתקדמים יותר ליצירת יישומים הניתנים להתאמה אישית ובטוחים מבחינת טיפוסים.
טיפוסים מותנים דיסטריבוטיביים
טיפוסים מותנים הם דיסטריבוטיביים (distributive) כאשר הטיפוס הנבדק הוא טיפוס איחוד. זה אומר שהתנאי מוחל על כל חבר באיחוד בנפרד, והתוצאות משולבות לאחר מכן לטיפוס איחוד חדש.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
בדוגמה זו, ToArray
מוחל על כל חבר באיחוד string | number
בנפרד, מה שמביא ל-string[] | number[]
. אם התנאי לא היה דיסטריבוטיבי, התוצאה הייתה (string | number)[]
.
שימוש בטיפוסי עזר (Utility Types)
TypeScript מספקת מספר טיפוסי עזר מובנים הממנפים טיפוסים ממופים וטיפוסים מותנים. ניתן להשתמש בטיפוסי עזר אלה כאבני בניין לטרנספורמציות טיפוסים מורכבות יותר.
Partial<T>
: הופך את כל המאפיינים שלT
לאופציונליים.Required<T>
: הופך את כל המאפיינים שלT
לחובה.Readonly<T>
: הופך את כל המאפיינים שלT
לקריאה-בלבד.Pick<T, K>
: בוחר קבוצת מאפייניםK
מתוךT
.Omit<T, K>
: מסיר קבוצת מאפייניםK
מתוךT
.Record<K, T>
: בונה טיפוס עם קבוצת מאפייניםK
מטיפוסT
.Exclude<T, U>
: מוציא מ-T
את כל הטיפוסים הניתנים להשמה ל-U
.Extract<T, U>
: מחלץ מ-T
את כל הטיפוסים הניתנים להשמה ל-U
.NonNullable<T>
: מוציא אתnull
ו-undefined
מ-T
.Parameters<T>
: משיג את הפרמטרים של טיפוס פונקציהT
.ReturnType<T>
: משיג את טיפוס ההחזרה של טיפוס פונקציהT
.InstanceType<T>
: משיג את טיפוס המופע של טיפוס פונקציה בנאיT
.
טיפוסי עזר אלה הם כלים רבי עוצמה שיכולים לפשט מניפולציות טיפוסים מורכבות. לדוגמה, ניתן לשלב את Pick
ו-Partial
כדי ליצור טיפוס שהופך רק מאפיינים מסוימים לאופציונליים:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
בדוגמה זו, ל-OptionalDescriptionProduct
יש את כל המאפיינים של Product
, אך המאפיין description
הוא אופציונלי.
שימוש בטיפוסי תבנית ליטרלית (Template Literal Types)
טיפוסי תבנית ליטרלית מאפשרים ליצור טיפוסים המבוססים על מחרוזות ליטרליות. ניתן להשתמש בהם בשילוב עם טיפוסים ממופים וטיפוסים מותנים כדי ליצור טרנספורמציות טיפוסים דינמיות ומלאות הבעה. לדוגמה, ניתן ליצור טיפוס שמוסיף קידומת מסוימת לכל שמות המאפיינים:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
בדוגמה זו, ל-PrefixedSettings
יהיו המאפיינים data_apiUrl
ו-data_timeout
.
שיטות עבודה מומלצות ושיקולים
- שמרו על פשטות: בעוד שטיפוסים ממופים וטיפוסים מותנים הם רבי עוצמה, הם יכולים גם להפוך את הקוד שלכם למורכב יותר. נסו לשמור על טרנספורמציות הטיפוסים שלכם פשוטות ככל האפשר.
- השתמשו בטיפוסי עזר: מנפו את טיפוסי העזר המובנים של TypeScript בכל הזדמנות אפשרית. הם נבדקו היטב ויכולים לפשט את הקוד שלכם.
- תעדו את הטיפוסים שלכם: תעדו בבירור את טרנספורמציות הטיפוסים שלכם, במיוחד אם הן מורכבות. זה יעזור למפתחים אחרים להבין את הקוד שלכם.
- בדקו את הטיפוסים שלכם: השתמשו בבדיקת הטיפוסים של TypeScript כדי להבטיח שטרנספורמציות הטיפוסים שלכם פועלות כמצופה. אתם יכולים לכתוב בדיקות יחידה כדי לאמת את התנהגות הטיפוסים שלכם.
- קחו בחשבון ביצועים: טרנספורמציות טיפוסים מורכבות יכולות להשפיע על ביצועי המהדר של TypeScript. היו מודעים למורכבות הטיפוסים שלכם והימנעו מחישובים מיותרים.
סיכום
טיפוסים ממופים וטיפוסים מותנים הם תכונות עוצמתיות ב-TypeScript המאפשרות ליצור טרנספורמציות טיפוסים גמישות ומלאות הבעה. על ידי שליטה במושגים אלה, תוכלו לשפר את בטיחות הטיפוסים, התחזוקתיות והאיכות הכללית של יישומי ה-TypeScript שלכם. מטרנספורמציות פשוטות כמו הפיכת מאפיינים לאופציונליים או לקריאה-בלבד, ועד לטרנספורמציות רקורסיביות מורכבות ולוגיקה מותנית, תכונות אלו מספקות את הכלים הדרושים לבניית יישומים חזקים וניתנים להרחבה. המשיכו לחקור ולהתנסות בתכונות אלו כדי למצות את מלוא הפוטנציאל שלהן ולהפוך למפתחי TypeScript מיומנים יותר.
בזמן שאתם ממשיכים במסע ה-TypeScript שלכם, זכרו למנף את שפע המשאבים הזמינים, כולל התיעוד הרשמי של TypeScript, קהילות מקוונות ופרויקטי קוד פתוח. אמצו את הכוח של טיפוסים ממופים וטיפוסים מותנים, ותהיו מצוידים היטב להתמודד גם עם הבעיות המאתגרות ביותר הקשורות לטיפוסים.