מעבר לטיפוסיות בסיסית. שלטו בתכונות TypeScript מתקדמות כמו סוגים מותנים, ליטרלי תבניות ומניפולציית מחרוזות לבניית ממשקי API חזקים ובטוחים מבחינה טיפוסית.
פתיחת מלוא הפוטנציאל של TypeScript: צלילה עמוקה לסוגים מותנים, ליטרלי תבניות ומניפולציה מתקדמת של מחרוזות
בעולם פיתוח התוכנה המודרני, TypeScript התפתחה הרבה מעבר לתפקידה הראשוני כבודק טיפוסים פשוט עבור JavaScript. היא הפכה לכלי מתוחכם למה שניתן לתאר כתכנות ברמת טיפוס. פרדיגמה זו מאפשרת למפתחים לכתוב קוד הפועל על טיפוסים עצמם, ויוצר ממשקי API דינמיים, המתעדים את עצמם ובטוחים להפליא. בליבת המהפכה הזו עומדות שלוש תכונות עוצמתיות הפועלות יחד: סוגים מותנים, סוגי ליטרל תבניות וסוויטה של סוגי מניפולציית מחרוזות אינהרנטיים.
עבור מפתחים ברחבי העולם המעוניינים לשדרג את כישורי TypeScript שלהם, הבנת מושגים אלה אינה עוד מותרות - זהו הכרח לבניית יישומים ניתנים להרחבה ותחזוקה. מדריך זה ייקח אתכם לצלילה עמוקה, החל מהעקרונות הבסיסיים ועד לדפוסים מורכבים מהעולם האמיתי המדגימים את העוצמה המשולבת שלהם. בין אם אתם בונים מערכת עיצוב, לקוח API בטוח מבחינה טיפוסית או ספריית טיפול בנתונים מורכבת, שליטה בתכונות אלה תשנה באופן מהותי את האופן שבו אתם כותבים TypeScript.
היסוד: סוגים מותנים (הטרינארי `extends`)
בבסיסה, סוג מותנה מאפשר לכם לבחור אחד משני סוגים אפשריים בהתבסס על בדיקת קשר בין סוגים. אם אתם מכירים את האופרטור הטרינארי של JavaScript (condition ? valueIfTrue : valueIfFalse), תמצאו את התחביר אינטואיטיבי באופן מיידי:
type Result = SomeType extends OtherType ? TrueType : FalseType;
כאן, מילת המפתח extends משמשת כתנאי שלנו. היא בודקת אם SomeType ניתן להקצאה ל-OtherType. בואו נפרק את זה עם דוגמה פשוטה.
דוגמה בסיסית: בדיקת טיפוס
תארו לעצמכם שאנו רוצים ליצור טיפוס שמחזיר true אם טיפוס נתון T הוא מחרוזת, ו-false אחרת.
type IsString
אנו יכולים להשתמש בטיפוס זה כך:
type A = IsString<"hello">; // type A is true
type B = IsString<123>; // type B is false
זהו אבן הבניין הבסיסית. אבל העוצמה האמיתית של טיפוסים מותנים משתחררת בשילוב עם מילת המפתח infer.
העוצמה של `infer`: חילוץ טיפוסים מבפנים
מילת המפתח infer היא מחליפת משחק. היא מאפשרת לכם להצהיר על משתנה טיפוס גנרי חדש בתוך סעיף ה-extends, ולמעשה ללכוד חלק מהטיפוס שאתם בודקים. תחשבו על זה כעל הצהרת משתנה ברמת הטיפוס שמקבל את הערך שלו מהתאמת דפוסים.
דוגמה קלאסית היא פריקת הטיפוס הכלול בתוך Promise.
type UnwrapPromise
בואו ננתח את זה:
T extends Promise: זה בודק אםTהואPromise. אם כן, TypeScript מנסה להתאים את המבנה.infer U: אם ההתאמה מצליחה, TypeScript לוכדת את הטיפוס שה-Promiseמחזיר ומכניסה אותו למשתנה טיפוס חדש בשםU.? U : T: אם התנאי מתקיים (TהיהPromise), הטיפוס המתקבל הואU(הטיפוס שנפרק). אחרת, הטיפוס המתקבל הוא פשוט הטיפוס המקוריT.
שימוש:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
דפוס זה נפוץ כל כך, ש-TypeScript כוללת טיפוסי כלי מובנים כמו ReturnType, שמיושם באמצעות אותו עיקרון כדי לחלץ את טיפוס ההחזרה של פונקציה.
טיפוסים מותנים דיסטריבוטיביים: עבודה עם איגודים
התנהגות מרתקת וחשובה של טיפוסים מותנים היא שהם הופכים דיסטריבוטיביים כאשר הטיפוס הנבדק הוא פרמטר טיפוס גנרי "עירום". משמעות הדבר היא שאם תעבירו אליו טיפוס איגוד, התנאי יוחל על כל איבר באיגוד בנפרד, והתוצאות ייאספו בחזרה לאיגוד חדש.
שקלו טיפוס שממיר טיפוס למערך של אותו טיפוס:
type ToArray
אם נעביר טיפוס איגוד ל-ToArray:
type StrOrNumArray = ToArray
התוצאה אינה (string | number)[]. מכיוון ש-T הוא פרמטר טיפוס עירום, התנאי מפוזר:
ToArrayהופך ל-string[]ToArrayהופך ל-number[]
התוצאה הסופית היא האיגוד של התוצאות הבודדות הללו: string[] | number[].
תכונה דיסטריבוטיבית זו שימושית להפליא לסינון איגודים. לדוגמה, טיפוס הכלי המובנה Extract משתמש בזה כדי לבחור איברים מאיגוד T שניתנים להקצאה ל-U.
אם אתם צריכים למנוע התנהגות דיסטריבוטיבית זו, אתם יכולים לעטוף את פרמטר הטיפוס בטאפל משני צידי סעיף ה-extends:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
עם יסוד מוצק זה, בואו נחקור כיצד אנו יכולים לבנות טיפוסי מחרוזת דינמיים.
בניית מחרוזות דינמיות ברמת הטיפוס: סוגי ליטרל תבניות
סוגי ליטרל תבניות, שהוצגו ב-TypeScript 4.1, מאפשרים לכם להגדיר טיפוסים המעוצבים כמו מחרוזות ליטרל תבניות של JavaScript. הם מאפשרים לכם לשרשר, לשלב וליצור טיפוסי ליטרל מחרוזת חדשים מקיימים.
התחביר הוא בדיוק מה שציפיתם:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting is "Hello, World!"
זה אולי נראה פשוט, אבל העוצמה שלו טמונה בשילוב שלו עם איגודים וגנריות.
איגודים ותמורות
כאשר טיפוס ליטרל תבנית כולל איגוד, הוא מתרחב לאיגוד חדש המכיל כל תמורה אפשרית של מחרוזות. זוהי דרך עוצמתית ליצור קבוצה של קבועים מוגדרים היטב.
תארו לעצמכם שמגדירים קבוצה של מאפייני שוליים של CSS:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
הטיפוס המתקבל עבור MarginProperty הוא:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
זה מושלם ליצירת אבזרי רכיב בטוחים מבחינה טיפוסית או ארגומנטים לפונקציה שבהם מותרים רק פורמטים ספציפיים של מחרוזות.
שילוב עם גנריות
ליטרלי תבניות באמת זורחים כאשר משתמשים בהם עם גנריות. אתם יכולים ליצור טיפוסי מפעל שיוצרים טיפוסי ליטרל מחרוזת חדשים בהתבסס על קלט כלשהו.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
דפוס זה הוא המפתח ליצירת ממשקי API דינמיים ובטוחים מבחינה טיפוסית. אבל מה אם אנחנו צריכים לשנות את רישיות המחרוזת, כמו לשנות "user" ל-"User" כדי לקבל "onUserChange"? שם נכנסים לתמונה טיפוסי מניפולציית מחרוזות.
ארגז הכלים: טיפוסי מניפולציית מחרוזות אינהרנטיים
כדי להפוך את ליטרלי התבניות לעוצמתיים עוד יותר, TypeScript מספקת קבוצה של טיפוסים מובנים לטיפול בליטרלי מחרוזות. אלה כמו פונקציות עזר אבל עבור מערכת הטיפוסים.
משני רישיות: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
ארבעת הטיפוסים האלה עושים בדיוק מה שהשמות שלהם מרמזים:
Uppercase: ממיר את טיפוס המחרוזת כולו לאותיות גדולות.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: ממיר את טיפוס המחרוזת כולו לאותיות קטנות.type quiet = Lowercase<"WORLD">; // "world"Capitalize: ממיר את התו הראשון של טיפוס המחרוזת לאות גדולה.type Proper = Capitalize<"john">; // "John"Uncapitalize: ממיר את התו הראשון של טיפוס המחרוזת לאות קטנה.type variable = Uncapitalize<"PersonName">; // "personName"
בואו נבקר שוב בדוגמה הקודמת שלנו ונשפר אותה באמצעות Capitalize כדי ליצור שמות קונבנציונליים של מטפלי אירועים:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
עכשיו יש לנו את כל החלקים. בואו נראה איך הם משתלבים כדי לפתור בעיות מורכבות מהעולם האמיתי.
הסינתזה: שילוב של שלושתם לדפוסים מתקדמים
כאן התיאוריה פוגשת את המעשה. על ידי שילוב של טיפוסים מותנים, ליטרלי תבניות ומניפולציית מחרוזות, אנו יכולים לבנות הגדרות טיפוסים מתוחכמות ובטוחות להפליא.
דפוס 1: פולט אירועים בטוח לחלוטין מבחינה טיפוסית
מטרה: צור מחלקה גנרית EventEmitter עם מתודות כמו on(), off() ו-emit() שהן בטוחות לחלוטין מבחינה טיפוסית. זה אומר:
- שם האירוע המועבר למתודות חייב להיות אירוע חוקי.
- המטען המועבר ל-
emit()חייב להתאים לטיפוס שהוגדר עבור אותו אירוע. - פונקציית הקריאה החוזרת המועברת ל-
on()חייבת לקבל את טיפוס המטען הנכון עבור אותו אירוע.
ראשית, אנו מגדירים מפה של שמות אירועים לטיפוסי המטען שלהם:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
כעת, אנו יכולים לבנות את המחלקה הגנרית EventEmitter. נשתמש בפרמטר גנרי Events שחייב להרחיב את מבנה ה-EventMap שלנו.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// מתודת `on` משתמשת ב-`K` גנרי שהוא מפתח של מפת ה-Events שלנו
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// מתודת `emit` מבטיחה שהמטען תואם לטיפוס האירוע
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
בואו ניצור מופע ונשתמש בו:
const appEvents = new TypedEventEmitter
// זה בטוח מבחינה טיפוסית. המטען מוערך כראוי כ- { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript תציג שגיאה כאן מכיוון ש-"user:updated" אינו מפתח ב-EventMap
// appEvents.on("user:updated", () => {}); // שגיאה!
// TypeScript תציג שגיאה כאן מכיוון שהמטען חסר את המאפיין 'name'
// appEvents.emit("user:created", { userId: 123 }); // שגיאה!
דפוס זה מספק בטיחות בזמן קומפילציה עבור מה שמסורתית הוא חלק דינמי ונוטה לשגיאות ביישומים רבים.
דפוס 2: גישה נתיב בטוחה מבחינה טיפוסית עבור אובייקטים מקוננים
מטרה: צור טיפוס כלי, PathValue, שיכול לקבוע את הטיפוס של ערך באובייקט מקונן T באמצעות נתיב מחרוזת בסימון נקודות P (לדוגמה, "user.address.city").
זהו דפוס מתקדם ביותר שמציג טיפוסים מותנים רקורסיביים.
הנה היישום, שאותו נפרוט:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
בואו נעקוב אחר הלוגיקה שלו עם דוגמה: PathValue
- קריאה ראשונית:
Pהוא"a.b.c". זה תואם לליטרל התבנית`${infer Key}.${infer Rest}`. Keyמוערך כ-"a".Restמוערך כ-"b.c".- רקורסיה ראשונה: הטיפוס בודק אם
"a"הוא מפתח שלMyObject. אם כן, הוא קורא באופן רקורסיבי ל-PathValue. - רקורסיה שנייה: עכשיו,
Pהוא"b.c". הוא תואם שוב לליטרל התבנית. Keyמוערך כ-"b".Restמוערך כ-"c".- הטיפוס בודק אם
"b"הוא מפתח שלMyObject["a"]וקורא באופן רקורסיבי ל-PathValue. - מקרה בסיסי: לבסוף,
Pהוא"c". זה לא תואם ל-`${infer Key}.${infer Rest}`. לוגיקת הטיפוס נופלת לתנאי השני:P extends keyof T ? T[P] : never. - הטיפוס בודק אם
"c"הוא מפתח שלMyObject["a"]["b"]. אם כן, התוצאה היאMyObject["a"]["b"]["c"]. אם לא, זהnever.
שימוש עם פונקציית עזר:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
טיפוס עוצמתי זה מונע שגיאות בזמן ריצה משגיאות כתיב בנתיבים ומספק היסק טיפוס מושלם עבור מבני נתונים מקוננים עמוקות, אתגר נפוץ ביישומים גלובליים המתמודדים עם תגובות API מורכבות.
שיטות עבודה מומלצות ושיקולי ביצועים
כמו בכל כלי רב עוצמה, חשוב להשתמש בתכונות אלה בתבונה.
- תעדוף קריאות: טיפוסים מורכבים יכולים להפוך לבלתי קריאים במהירות. חלקו אותם לטיפוסי עזר קטנים יותר בעלי שמות טובים. השתמשו בהערות כדי להסביר את הלוגיקה, בדיוק כפי שהייתם עושים עם קוד זמן ריצה מורכב.
- הבינו את טיפוס ה-`never`: טיפוס ה-
neverהוא הכלי העיקרי שלכם לטיפול במצבי שגיאה ולסינון איגודים בטיפוסים מותנים. הוא מייצג מצב שלעולם לא אמור להתרחש. - היזהרו ממגבלות רקורסיה: ל-TypeScript יש מגבלת עומק רקורסיה עבור יצירת טיפוסים. אם הטיפוסים שלכם מקוננים עמוק מדי או רקורסיביים לאין שיעור, הקומפיילר יציג שגיאה. ודאו שלטיפוסים הרקורסיביים שלכם יש מקרה בסיסי ברור.
- עקבו אחר ביצועי IDE: טיפוסים מורכבים במיוחד יכולים לפעמים להשפיע על הביצועים של שרת השפה של TypeScript, מה שמוביל להשלמה אוטומטית ובדיקת טיפוסים איטיות יותר בעורך שלכם. אם אתם חווים האטה, בדקו אם ניתן לפשט או לחלק טיפוס מורכב.
- דעו מתי לעצור: תכונות אלה נועדו לפתור בעיות מורכבות של בטיחות טיפוסים וחוויית מפתח. אל תשתמשו בהן כדי לעצב יתר על המידה טיפוסים פשוטים. המטרה היא לשפר את הבהירות והבטיחות, לא להוסיף מורכבות מיותרת.
מסקנה
טיפוסים מותנים, ליטרלי תבניות וטיפוסי מניפולציית מחרוזות אינם רק תכונות מבודדות; הם מערכת משולבת הדוקה לביצוע לוגיקה מתוחכמת ברמת הטיפוס. הם מעצימים אותנו להתקדם מעבר להערות פשוטות ולבנות מערכות המודעות עמוקות למבנה ולאילוצים שלהן.
על ידי שליטה בשלישייה זו, אתם יכולים:
- ליצור ממשקי API המתעדים את עצמם: הטיפוסים עצמם הופכים לתיעוד, ומנחים מפתחים להשתמש בהם בצורה נכונה.
- לחסל מחלקות שלמות של באגים: שגיאות טיפוסים נתפסות בזמן קומפילציה, לא על ידי משתמשים בייצור.
- לשפר את חוויית המפתח: תיהנו מהשלמה אוטומטית עשירה והודעות שגיאה מוטבעות גם עבור החלקים הדינמיים ביותר של בסיס הקוד שלכם.
אימוץ היכולות המתקדמות הללו הופך את TypeScript מרשת ביטחון לשותף רב עוצמה בפיתוח. זה מאפשר לכם לקודד לוגיקה עסקית מורכבת ואינווריאנטים ישירות למערכת הטיפוסים, מה שמבטיח שהיישומים שלכם יהיו חזקים, ניתנים לתחזוקה וניתנים להרחבה יותר עבור קהל עולמי.