גלו את העוצמה של TypeScript עם טיפוסים מותנים וממופים מתקדמים. למדו ליצור יישומים גמישים ובטוחים שמתאימים למבני נתונים מורכבים. שלטו באמנות כתיבת קוד TypeScript דינמי באמת.
תבניות TypeScript מתקדמות: שליטה בטיפוסים מותנים וממופים
העוצמה של TypeScript טמונה ביכולתה לספק טיפוסיות חזקה (strong typing), המאפשרת לכם לתפוס שגיאות מוקדם ולכתוב קוד קריא וקל יותר לתחזוקה. בעוד שטיפוסים בסיסיים כמו string
, number
, ו-boolean
הם יסודיים, התכונות המתקדמות של TypeScript כמו טיפוסים מותנים וממופים פותחות מימד חדש של גמישות ובטיחות טיפוסים. מדריך מקיף זה יעמיק במושגים רבי עוצמה אלה, ויצייד אתכם בידע ליצירת יישומי TypeScript דינמיים וסתגלניים באמת.
מהם טיפוסים מותנים?
טיפוסים מותנים מאפשרים להגדיר טיפוסים התלויים בתנאי, בדומה לאופרטור טרנרי ב-JavaScript (condition ? trueValue : falseValue
). הם מאפשרים לבטא יחסי טיפוסים מורכבים המבוססים על השאלה האם טיפוס מסוים עומד באילוץ ספציפי.
תחביר
התחביר הבסיסי עבור טיפוס מותנה הוא:
T extends U ? X : Y
T
: הטיפוס הנבדק.U
: הטיפוס שלפיו מתבצעת הבדיקה.extends
: מילת המפתח המציינת יחס של תת-טיפוס (subtype).X
: הטיפוס שבו יש להשתמש אםT
ניתן להשמה ל-U
.Y
: הטיפוס שבו יש להשתמש אםT
אינו ניתן להשמה ל-U
.
במהותו, אם T extends U
מתקבל כ-true, הטיפוס יתורגם ל-X
; אחרת, הוא יתורגם ל-Y
.
דוגמאות מעשיות
1. קביעת הטיפוס של פרמטר בפונקציה
נניח שברצונכם ליצור טיפוס שקובע אם פרמטר של פונקציה הוא מחרוזת או מספר:
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Value is a string:", value);
} else {
console.log("Value is a number:", value);
}
}
processValue("hello"); // Output: Value is a string: hello
processValue(123); // Output: Value is a number: 123
בדוגמה זו, ParamType<T>
הוא טיפוס מותנה. אם T
הוא מחרוזת, הטיפוס מתורגם ל-string
; אחרת, הוא מתורגם ל-number
. הפונקציה processValue
מקבלת מחרוזת או מספר בהתבסס על טיפוס מותנה זה.
2. חילוץ טיפוס החזרה בהתבסס על טיפוס הקלט
דמיינו תרחיש שבו יש לכם פונקציה שמחזירה טיפוסים שונים בהתבסס על הקלט. טיפוסים מותנים יכולים לעזור לכם להגדיר את טיפוס ההחזרה הנכון:
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Output: 7
console.log(numberProcessor.process(42)); // Output: "42"
כאן, הטיפוס Processor<T>
בוחר באופן מותנה בין StringProcessor
או NumberProcessor
בהתבסס על טיפוס הקלט. זה מבטיח שהפונקציה createProcessor
תחזיר את אובייקט המעבד מהטיפוס הנכון.
3. איחודים מובחנים (Discriminated Unions)
טיפוסים מותנים הם חזקים במיוחד בעבודה עם איחודים מובחנים. איחוד מובחן הוא טיפוס איחוד (union) שבו לכל חבר יש מאפיין משותף בעל טיפוס יחידני (המבחין). זה מאפשר לצמצם את הטיפוס בהתבסס על ערך המאפיין הזה.
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
בדוגמה זו, הטיפוס Shape
הוא איחוד מובחן. הטיפוס Area<T>
משתמש בטיפוס מותנה כדי לקבוע אם הצורה היא ריבוע או עיגול, ומחזיר number
עבור ריבועים ו-string
עבור עיגולים (למרות שבתרחיש אמיתי, סביר להניח שהייתם רוצים טיפוסי החזרה עקביים, זה מדגים את העיקרון).
נקודות מרכזיות לגבי טיפוסים מותנים
- מאפשרים הגדרת טיפוסים בהתבסס על תנאים.
- משפרים את בטיחות הטיפוסים על ידי ביטוי יחסי טיפוסים מורכבים.
- שימושיים לעבודה עם פרמטרים של פונקציות, טיפוסי החזרה ואיחודים מובחנים.
מהם טיפוסים ממופים?
טיפוסים ממופים מספקים דרך לשנות טיפוסים קיימים על ידי מיפוי על פני המאפיינים שלהם. הם מאפשרים ליצור טיפוסים חדשים המבוססים על המאפיינים של טיפוס אחר, תוך החלת שינויים כמו הפיכת מאפיינים לאופציונליים, לקריאה בלבד (readonly), או שינוי הטיפוסים שלהם.
תחביר
התחביר הכללי עבור טיפוס ממופה הוא:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
: טיפוס הקלט.keyof T
: אופרטור טיפוסים שמחזיר איחוד של כל מפתחות המאפיינים ב-T
.K in keyof T
: עובר על כל מפתח ב-keyof T
, ומקצה כל מפתח למשתנה הטיפוסK
.ModifiedType
: הטיפוס שאליו ימופה כל מאפיין. זה יכול לכלול טיפוסים מותנים או שינויי טיפוסים אחרים.
דוגמאות מעשיות
1. הפיכת מאפיינים לאופציונליים
ניתן להשתמש בטיפוס ממופה כדי להפוך את כל המאפיינים של טיפוס קיים לאופציונליים:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Valid, as 'id' and 'email' are optional
כאן, PartialUser
הוא טיפוס ממופה שעובר על המפתחות של הממשק User
. עבור כל מפתח K
, הוא הופך את המאפיין לאופציונלי על ידי הוספת המודיפיקטור ?
. הביטוי User[K]
מאחזר את הטיפוס של המאפיין K
מהממשק User
.
2. הפיכת מאפיינים לקריאה בלבד (Readonly)
באופן דומה, ניתן להפוך את כל המאפיינים של טיפוס קיים לקריאה בלבד:
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Example Product",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Error: Cannot assign to 'price' because it is a read-only property.
במקרה זה, ReadonlyProduct
הוא טיפוס ממופה שמוסיף את המודיפיקטור readonly
לכל מאפיין של הממשק Product
.
3. שינוי טיפוסי מאפיינים
ניתן להשתמש בטיפוסים ממופים גם כדי לשנות את הטיפוסים של מאפיינים. לדוגמה, ניתן ליצור טיפוס שממיר את כל מאפייני המחרוזת למספרים:
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Must be a number due to the mapping
timeout: 456, // Must be a number due to the mapping
maxRetries: 3,
};
דוגמה זו מדגימה שימוש בטיפוס מותנה בתוך טיפוס ממופה. עבור כל מאפיין K
, הוא בודק אם הטיפוס של Config[K]
הוא מחרוזת. אם כן, הטיפוס ממופה ל-number
; אחרת, הוא נשאר ללא שינוי.
4. מיפוי מפתחות מחדש (החל מ-TypeScript 4.1)
TypeScript 4.1 הציגה את היכולת למפות מחדש מפתחות בתוך טיפוסים ממופים באמצעות מילת המפתח as
. זה מאפשר ליצור טיפוסים חדשים עם שמות מאפיינים שונים המבוססים על הטיפוס המקורי.
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Result:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize function used to Capitalize first letter
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Usage with an actual object
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
כאן, הטיפוס TransformedEvent
ממפה מחדש כל מפתח K
למפתח חדש עם הקידומת "new" ועם אות גדולה בתחילתו. פונקציית העזר `Capitalize` מבטיחה שהאות הראשונה של המפתח תהפוך לגדולה. החיתוך string & K
מבטיח שאנו עוסקים רק במפתחות מסוג מחרוזת ושאנו מקבלים את הטיפוס הליטרלי הנכון מ-K.
מיפוי מפתחות מחדש פותח אפשרויות עוצמתיות לשינוי והתאמה של טיפוסים לצרכים ספציפיים. זה מאפשר לכם לשנות שמות, לסנן או לשנות מפתחות על בסיס לוגיקה מורכבת.
נקודות מרכזיות לגבי טיפוסים ממופים
- מאפשרים לשנות טיפוסים קיימים על ידי מיפוי על פני המאפיינים שלהם.
- מאפשרים להפוך מאפיינים לאופציונליים, לקריאה בלבד, או לשנות את הטיפוסים שלהם.
- שימושיים ליצירת טיפוסים חדשים המבוססים על המאפיינים של טיפוס אחר.
- מיפוי מפתחות מחדש (שהוכנס ב-TypeScript 4.1) מציע גמישות רבה עוד יותר בשינויי טיפוסים.
שילוב טיפוסים מותנים וממופים
העוצמה האמיתית של טיפוסים מותנים וממופים באה לידי ביטוי כאשר משלבים אותם. זה מאפשר ליצור הגדרות טיפוסים גמישות ובעלות הבעה גבוהה שיכולות להתאים למגוון רחב של תרחישים.דוגמה: סינון מאפיינים לפי טיפוס
נניח שברצונכם ליצור טיפוס שמסנן את המאפיינים של אובייקט על בסיס הטיפוס שלהם. לדוגמה, ייתכן שתרצו לחלץ רק את מאפייני המחרוזת מאובייקט.
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Result:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
בדוגמה זו, הטיפוס StringProperties<T>
משתמש בטיפוס ממופה עם מיפוי מפתחות מחדש וטיפוס מותנה. עבור כל מאפיין K
, הוא בודק אם הטיפוס של T[K]
הוא מחרוזת. אם כן, המפתח נשמר; אחרת, הוא ממופה ל-never
, מה שבפועל מסנן אותו החוצה. מיפוי מפתח ל-never
בטיפוס ממופה מסיר אותו מהטיפוס הסופי. זה מבטיח שרק מאפייני מחרוזת ייכללו בטיפוס StringData
.
טיפוסי עזר (Utility Types) ב-TypeScript
TypeScript מספקת מספר טיפוסי עזר מובנים הממנפים טיפוסים מותנים וממופים לביצוע שינויי טיפוסים נפוצים. הבנת טיפוסי עזר אלה יכולה לפשט משמעותית את הקוד שלכם ולשפר את בטיחות הטיפוסים.
טיפוסי עזר נפוצים
Partial<T>
: הופך את כל המאפיינים שלT
לאופציונליים.Readonly<T>
: הופך את כל המאפיינים שלT
לקריאה בלבד.Required<T>
: הופך את כל המאפיינים שלT
לחובה (מסיר את המודיפיקטור?
).Pick<T, K extends keyof T>
: בוחר קבוצת מאפייניםK
מתוךT
.Omit<T, K extends keyof T>
: מסיר קבוצת מאפייניםK
מתוךT
.Record<K extends keyof any, T>
: בונה טיפוס עם קבוצת מאפייניםK
מהטיפוסT
.Exclude<T, U>
: מחריג מ-T
את כל הטיפוסים שניתן להשים ל-U
.Extract<T, U>
: מחלץ מ-T
את כל הטיפוסים שניתן להשים ל-U
.NonNullable<T>
: מחריגnull
ו-undefined
מ-T
.Parameters<T>
: מקבל את הפרמטרים של טיפוס פונקציהT
בתוך tuple.ReturnType<T>
: מקבל את טיפוס ההחזרה של טיפוס פונקציהT
.InstanceType<T>
: מקבל את טיפוס המופע של טיפוס פונקציית בנאיT
.ThisType<T>
: משמש כסמן עבור טיפוסthis
קונטקסטואלי.
טיפוסי עזר אלה בנויים באמצעות טיפוסים מותנים וממופים, מה שמדגים את העוצמה והגמישות של תכונות TypeScript מתקדמות אלו. לדוגמה, Partial<T>
מוגדר כך:
type Partial<T> = {
[P in keyof T]?: T[P];
};
שיטות עבודה מומלצות לשימוש בטיפוסים מותנים וממופים
בעוד שטיפוסים מותנים וממופים הם רבי עוצמה, הם יכולים גם להפוך את הקוד שלכם למורכב יותר אם לא משתמשים בהם בזהירות. הנה כמה שיטות עבודה מומלצות שכדאי לזכור:
- שמרו על פשטות: הימנעו מטיפוסים מותנים וממופים מורכבים מדי. אם הגדרת טיפוס הופכת למסורבלת מדי, שקלו לפרק אותה לחלקים קטנים וקלים יותר לניהול.
- השתמשו בשמות משמעותיים: תנו לטיפוסים המותנים והממופים שלכם שמות תיאוריים המציינים בבירור את מטרתם.
- תעדו את הטיפוסים שלכם: הוסיפו הערות כדי להסביר את הלוגיקה שמאחורי הטיפוסים המותנים והממופים שלכם, במיוחד אם הם מורכבים.
- נצלו טיפוסי עזר: לפני יצירת טיפוס מותנה או ממופה מותאם אישית, בדקו אם טיפוס עזר מובנה יכול להשיג את אותה תוצאה.
- בדקו את הטיפוסים שלכם: ודאו שהטיפוסים המותנים והממופים שלכם מתנהגים כצפוי על ידי כתיבת בדיקות יחידה המכסות תרחישים שונים.
- שקלו ביצועים: חישובי טיפוסים מורכבים יכולים להשפיע על זמני הקומפילציה. היו מודעים להשלכות הביצועים של הגדרות הטיפוסים שלכם.
סיכום
טיפוסים מותנים וממופים הם כלים חיוניים לשליטה ב-TypeScript. הם מאפשרים לכם ליצור יישומים גמישים, בטוחים מבחינת טיפוסים וקלים לתחזוקה, המתאימים למבני נתונים מורכבים ודרישות דינמיות. על ידי הבנה ויישום של המושגים שנדונו במדריך זה, תוכלו למנף את מלוא הפוטנציאל של TypeScript ולכתוב קוד חזק וסקיילבילי יותר. ככל שתמשיכו לחקור את TypeScript, זכרו להתנסות בשילובים שונים של טיפוסים מותנים וממופים כדי לגלות דרכים חדשות לפתור בעיות טיפוסיות מאתגרות. האפשרויות הן באמת אינסופיות.