במאמרים הקודמים בסדרת המדריכים על טייפסקריפט למדנו על סוגי מידע בסיסיים ואכיפתם בפונקציות ובמשתנים, למדנו גם על בניית סביבת עבודה בסיסית ב-tsc. במדריך הזה אנו נמשיך את הלימוד בכל הנוגע לסוגים שונים.
Type Aliases
כאשר אנו מגדירים אובייקט, אנו יכולים לעשות זאת בקלות בדרך שבה למדנו – בהגדרת הקלט של הפונקציה. כך למשל:
const squareSizeCalculator = (square: {x:number, y:number}):number => {
return square.x*square.y;
}
let result = squareSizeCalculator({x:2,y:3});
console.log(result);
אבל אפשר לראות שזה טיפה בעייתי. למה? כי אם יש לנו אובייקט מורכב יחסית, אז אנחנו בבעיה:
const userOutput = (userObject: {firstName: string, lastName: string, id: string | number, city: string, zip: number, phone: string}) {
// do something
}
גם מתישהו אנו נצטרך אולי למחזר את האובייקט הזה. אולי הוא מופיע בעוד מקומות. בדיוק בשביל זה אנחנו יכולים להגדיר סוג של אובייקט (type) באופן נפרד. איך? ממש כמו משתנה, רק במקום const או let (שאף אחד לא יזכיר פה את var!!!) יהיה לנו את המשתנה הזה, שנקרא aliases. שם נגדיר את כל מה שצריך ונוכל להשתמש ב-aliases הזה איפה שנרצה. הנה דוגמה להמחשה:
type user = {
firstName: string,
lastName: string,
id: string | number,
city: string,
zip: number,
phone: string
}
const userOutput = (userObject: user) => {
// do something
}
userOutput({
firstName: 'Ran',
lastName: 'Bar-Zik',
id: '6382020',
city: 'Petah Tikva',
zip: 6382020,
phone: '03-6382020'
});
שימו לב ש-aliases זה לא ג'אווהסקריפט. בקובץ המקומפל לא תראו את הגדרת ה-type בכלל. זה פשוט טייפסקריפט שקובע איך אובייקטים נראים.
אפשר לקבוע type לא רק לאובייקטים אלא גם באופן כללי. למשל:
type ID = number | string;
אני אוכל להשתמש ב-ID הזה בכל פעם שיש לי number | string. אם אי פעם ארצה לשנות את זה, אני אוכל במקום אחד. הנה דוגמה לשימוש:
type ID = number | string;
type User = {
firstName: string,
lastName: string,
id: ID,
city: string,
zip: number,
phone: string
}
וזה כבר מתחיל להיות מעניין, כי במקום להגדיר כל פעם ממשק עבור פונקציה, אני מגדיר באותו מקום.
Interfaces
יחד עם alias שאותו אנו מכירים יש לנו גם interface. הוא עובד באופן די דומה ל-type. לא משתמשים ב-= אלא במילה השמורה interface. הנה, כך:
interface User {
firstName: string,
lastName: string,
id: ID,
city: string,
zip: number,
phone: string
}
const userOutput = (userObject: User) => {
// do something
}
userOutput({
firstName: 'Ran',
lastName: 'Bar-Zik',
id: '6382020',
city: 'Petah Tikva',
zip: 6382020,
phone: '03-6382020'
});
ההבדל העיקרי בינהם הוא ש-interface אפשר להרחיב עם interface אחר באמצעות extends. באופן די קל ופשוט. הנה דוגמה שתבהיר את הכל:
type ID = number | string;
interface User {
firstName: string,
lastName: string,
id: ID,
city: string,
zip: number,
phone: string
}
interface PremiumUser extends User {
subsriptionId: string,
subscriptionStatus: string
}
const userOutput = (userObject: PremiumUser) => {
// do something
}
userOutput({
firstName: 'Ran',
lastName: 'Bar-Zik',
id: '6382020',
city: 'Petah Tikva',
zip: 6382020,
phone: '03-6382020',
subsriptionId: '6382020',
subscriptionStatus: 'gold'
});
במקרה הזה יצרתי אינטרפייס חדש שנקרא PremiumUser שפשוט הוא user עם עוד שתי שדות רלוונטיים לסוג הזה.
שימו לב: מקובל להשתמש באות גדולה בהגדרת type alias או interface.
Type Assertions
בפוסט על הגדרות משתנים, אמרנו ש"גם אם לא הגדרתם סוג, טייפסקריפט תגדיר את סוג המשתנה לפי ההשמה הראשונה. כך למשל, אם הגדרתי את someVar שיקבל ערך 5, מבלי להגדיר לו סוג, אז אם בהמשך אני אנסה להכניס אליו ערך לא מספרי, אני אקבל שגיאת טייפסקריפט." זה קורה כמובן גם בפלט של פונקציות מובנות בשפה. טייפסקריפט פשוט תנחש את סוג הפלט שיוצא ולפעמים זה פחות מתאים לנו. זו הסיבה שאנו יכולים ב-override. הוא נעשה עם as. למשל (דוגמה מתוך הדוקומנטציה). הפונקציה getElementById היא פונקציה טבעית בג'אווהסקריפט (מבוסס דפדפן) מחזירה לנו אלמנט DOM. אם נשתמש בה באופן הזה:
const myCanvas = document.getElementById("main_canvas");
אז טייפסקריפט תנסה לנחש את הסוג שחוזר, שזה אלמנט DOM אז מדובר באובייקט מסוג HTMLElement. אבל אם אנו יודעים שאנו קוראים לאובייקט HTMLElement מסוג קנבס, אנו יכולים לצמצם את הסוג ל: HTMLCanvasElement. אבל איך עושים את זה? מגדירים לטייפסקריפט שבמקרה הזה היא לא מנחשת. במקרה הזה אנחנו אומרים לה מה הסוג. איך? עם המילה השמורה as ואז שם הסוג:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
הדגמה נוספת היא למשל אלמנט שהוגדר באופן לא נכון ואנו רוצים לשנות אותו. בקוד הזה הגדרנו אובייקט, אובייקט הוא גם מערך. אבל כשאנו רוצים להשתמש בתכונת length אני אקבל שגיאה. מה הפתרון? להסב את הסוג למערך. איך? ככה:
const data: object = ['a', 'b', 'c'];
// data.length; // Error
console.log((data as string[]).length); // 3
ההסבה הזו היא חד פעמית ואינה משפיעה על הקוד של ג'אווהסקריפט.
דרך נוספת לבצע השמה של משתנה היא באמצעות סוגריים משולשים (עם חיצים) באופן הבא:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
או:
console.log((<Array<string>>data).length);
אני ארחיב על הסינטקס הזה יותר מאוחר. אבל כשאנו מציבים חיצים לפני משתנה או פקודה, אנו מגדירים לטייפסקריפט סוג.
הגדרה של סוג קבוע
אנחנו יכולים להגדיר סוג קבוע, מה זאת אומרת? סוג שמכיל ערך ספציפי. למשל משתנה שהסוג שלו הוא מחרוזת טקסט ספציפית ממש. למשל בדוגמה הבאה, אני מגדיר משתנה שיכול לקבל רק טקסט כסוג ורק טקסט מסוג מסוים שהוא world. אם אני אנסה להכניס טקסט אחר, תהיה לי שגיאה:
let a: 'world' = 'world';
a = 'hello'; // Type '"hello"' is not assignable to type '"world"'.
זה נשמע כמו הפיצ'ר הכי מטופש בעולם. כי מי בדיוק ירצה סוג שיכול להיות טקסט מסוג אחד? אבל אם נשלב את זה עם union נוכל להגיע למצב שבו אני יכול להגדיר למשתנה כמה סוגים. למשל – משתנה שיכול להכיל רק מידות של בגדים שהגדרנו מראש. למשל:
let a: 'small' | 'medium' | 'large';
a = 'small';
a = 'medium';
a = 'large';
a = 'big'; // Type '"bug"' is not assignable to type '"small" | "medium" | "large"'.
זה נחמד, לא?
הגדרה של ערך קבוע באובייקטים
לפעמים, בדיוק כמו במשתנים, אנו רוצים לקבע ערך קבוע בתכונה מסוימת באובייקט. למשל במקרה שבו יש לי אובייקט מסוג userObject ולו יש תכונה בשם userType. אני רוצה שלתכונה הזו יהיה אפשר לשים רק admin. מן הסתם, אני אבצע הגדרה של סוג קבוע כמו בסעיף הקודם. הנה דוגמה:
const userObject = { userType: 'admin' };
// Later on in the code
userObject.userType = 'user';
אבל שוד ושבר!נ זה לא יחזיר שגיאה. למרות שהגדרתי סוג קבוע. הפתרון? שימוש ב-as להגדרה קבועה. כך:
const userObject = { userType : 'admin' as 'admin'};
// Later on in the code
userObject.userType = 'user'; // Type '"user"' is not assignable to type '"admin"'.
אני יכול לעשות גם union פה כדי להגביל סוגים:
const userObject = { userType : 'admin' as 'admin' | 'user' as 'user'};
// Later on in the code
userObject.userType = 'something'; // Type '"something"' is not assignable to type '"user"'.
עד כאן בנוגע לסוגי נתונים ועבודה איתם. בגדול למדנו את כל הבסיס, עכשיו הגיע הזמן להתקדם הלאה.
במאמר הבא אנו נלמד על פונקציות עם טייפסקריפט. נכנס קצת יותר לעומק של איך טייפסקריפט מתנהג בפלט, קלט ובכלל בתוך מבני הפונקציה של ג'אווהסקריפט.
5 תגובות
אחלה מאמר, כתוב מעולה, עבודה יפה!
חשוב לציין שהנקודה לגבי ההבדל שבין אינטרפייסים לטייפים שאי אפשר להרחיב טייפים הוא לא נכון. אפשר – טייפים משתמשים ב& במקום במילת הקוד extends.
הנה חלק מהדוקומטנציה הרשמית שמסביר את ההבדלים:
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces
תודה רבה על הפוסט 🙂
אני חדשה יחסית ב- TS ורציתי לשאול – מתי נבחר להשתמש בinterface במקום בclass בts? זה כדי להימנע מהתעסקות עם ירושה?
נתקלתי במצב בו רציתי לבדוק אם אובייקט הוא מטיפוס א׳ או ב׳, אבל בגלל ששני הטיפוסים היו interfaces לא יכלתי להשתמש בפתרון מהיר כמו אופרטור typeof.
יש עוד הבדל שהוא מהותי בין interfaces לtype aliases. שinterface הוא לא distrebuted. זה אומר שהכל מושטח (אין union למשל). מה שגם הופך אותו ליותר מהיר (interfaces מומלצים על ידי צוות הTS). בנוסף לא הייתי משתמש בas בשביל null assertion, אלא ב!. כי זה as מוריד משמעותית מהtype safety. ועוד דבר, as const הרבה יותר נחמד מאשר כל פעם לעשות copy & paste.
וידאו טוב על distribution
https://www.youtube.com/watch?v=IsnyTZi84ZY
יפה יפה 🙂
כמה הערות:
1. כדאי להזכיר שאין אפשרות לעשות union בinterfaces (משהו כמו interface A {} | {}) וזה לפעמים מעצבן.
2. אפשר לדמות extends בלי interfaces בעזרת intersection – כלומר type A = SuperType & {…}.
3. הקטע הכי מגניב לconst strings זה כשזה מגיע עם interpolation – לדוג' אפשר לעשות:
type Unit = "px" | "em" | …;
type CSSValue = `${number}{Unit}`;
ואז "123px" מתקבל אבל "123xp" או "12px3" לא (ההגדרה הזו לא מושלמת אבל אפשר להרחיב אותה, יש אנשים שכתבו דברים הזויים כמו JSON parsers עם זה, זה אפילו turing complete).
4. במקום לחזור על הערך עם as "string", הדרך המקובלת היא לכתוב as const.
5. אם אני זוכר נכון, ההמרה עם הסוגריים המשולשים לא עובדת עם jsx.
"שימו לב: מקובל להשתמש באות גדולה בהגדרת type alias או interface" – הדוגמה הראשונה בדף הזה היא user שמתחילה באות קטנה…