- בעמוד הזה תמצאו כללים בכתיבת קוד TypeScript.
- הכללים כתובים בצורה קצרה כדי שיהיה אפשר למצוא במהירות את מה שחיפשנו.
- לכל כלל יש דוגמה קצרה לשיפור ההבנה ושיהיה אפשר לעשות העתק הדבק.
- אם יש כלל מסובך אז נוסיף גם דוגמה מפורטת יותר לצורך הסבר בהיר יותר.
- הדף ילך ויתארך במשך הזמן... 😉
סוגי המשתנים הקיימים הם:
- Boolean - משתנה בוליאני.
let firstTime: boolean = true;
- Number - כל סוגי המספרים.
let decimal: number = 6; let hex: number = 0xf00d; let binary: number = 0b1010; let octal: number = 0o744;
- String - טקסט. אפשר להשתמש בגרש יחיד (') או גרשיים ("). ניתן גם להקיף את הטקסט ב-backtick (גרש אחורי שנראה כך `) כדי לכתובת טקסט על פני כמה שורות.
let firstName: string = "Yosef"; let firstName: string = 'Yosef'; let sixDayWar: string = `The Six-Day War, also known as the June War, 1976 Arab-Israeli War, or Third Arab-Israeli War, was fought between June 5 and 10, 1967 by Israel and the neighboring states of Egypt, Jordan, and Syria`;
- Array - ניתן להגדיר מערך מכל סוג. יש שני דרכים להגדיר מערך:
let oddNumbers: number[] = [1, 3, 5, 7, 9]; let oddNumbers: Array<number> = [1, 3, 5, 7, 9];
- Tuple - מערך שמורכב מכמה סוגי משתנים.
let person: [string, number]; person = ["Avraham", 53];
אם נהפוך את הסדר נקבל שגיאה:
person = [53, "Avraham"]; // Error
person[2] = "Yaakov"; person[3] = 37; person[3] = true; //Error - boolean is not string or number
- Enum - דרך טובה לתת שמות למספרים:
enum days {Sunday, Monday, Tuesday, Wednesday};// 0,1,2,3 let today: days = days.Monday;
אם לא מציינים אחרת המיספור מתחיל מ-0. ולכן בדוגמא שלנו today יהיה שווה ל-1. ניתן לקבוע מאיזה מספר יתחיל המיספור:
enum days {Sunday = 1, Monday, Tuesday, Wednesday};// 1,2,3,4
וניתן גם לקבוע מספרים לכל שאר הערכים:
enum days {Sunday = 1, Monday = 2, Tuesday = 3, Wednesday = 4};
יש פיצ'ר שימושי ל-enum שניתן לקבל בקלות את השם של אותו האנומרציה:
enum days {Sunday = 1, Monday, Tuesday, Wednesday}; let todayName: string = days[2]; // todayName = "Monday"
- Any - משתנה מכל סוג שהוא. נשתמש בסוג הזה כשאנחנו לא יודעים איזה סוג המשתנה הזה יהיה. זה בעצם דומה לכל המשתנים ב-JavaScript, שאין בדיקה של סוג הערכים ששמים לתוך המשתנים.
let whatever: any; whatever = "good morning"; // OK whatever = 7; // OK whatever = false; // OK
זה גם שימושי למערך שאיננו יודעים מה יהיה בתוכו:
let supriseList: any[] = [53, "Avraham", true]; supriseList[1] = 39; // OK, since there is no type checking
- Void - בדרך כלל משמש להגדיר שפונקציה מסוימת לא מחזירה ערך. אם מגידירים משתנה מסוג void אז הוא לא כל כך שימושי כי הוא יכול לקבל רק undefined או null.
let unusable: void = undefined;
- Null and Undefined - שני הסוגים האלו הם בעצם גם הסוג של עצמם. הכוונה היא ש-null הוא מסוג null ו-undefined הוא מסוג undefined. אם נגדיר משתנה כאחד משני הסוגים האלו אז הוא לא יהיה כל כך שימושי כיון שיהיה אפשר להכניס אליו רק את אותו הסוג: null יקבל רק null, ו-undefined יקבל רק undefined.
let u: undefined = undefined; let n: null = null;
שני הסוגים האלו הם subtypes של כל הסוגים האחרים. המשמעות היא שאפשר להכניס אותם למשתנה מכל סוג.
- Never - הסוג הזה מייצג משהו שבעצם לא יכול לקרות. חייבים פה דוגמה כדי להבין במה מדובר. לדוגמה, פונקציה שלא חוזרת אף פעם תהיה בעלת סוג משתנה never ב-return value. גם never הוא subtype של כל שאר הסוגים, אבל שום סוג אחר לא יכול להיכנס למשתנה מסוג never (גם לא any). דוגמאות לפונקציות שסוג ה-return שלהם הוא never:
// Functions with unreachable end point function error(message: string): never { throw new error(message); } function fail() { return error("Something failed"); } function infiniteLoop(): never { while (true) { } }
Type assertions
הביטוי type assertions דומה למה שנקרא casting. זו צורה להגיד לקומפיילר שיתייחס למשתנה מסוים לפי סוג מסוים. לדוגמה:
let someVar: any = "my string"; let strLen: number = (<string>someVar).length;
בצורה הזו אנו מודיעים לקומפיילר שיתייחס למשתנה someVar כ-string.
יש צורת כתיבה נוספת:
let someVar: any = "my string"; let strLen: number = (someVar as string).length;
let
ב-TypeScript משתמשים בעיקר ב-let כדי להגדיר משתנים במקום ב-var.
להלן כמה נקודות חשובות בנוגע לשימוש ב-let:
להלן כמה נקודות חשובות בנוגע לשימוש ב-let:
- משתנה שמוגדר ע"י let קיים רק בתוך אותו בלוק שבו הוא מוגדר (בשונה מ-var ששם ניתן להשתמש במשתנה בתוך כל אותה הפונקציה שבה הוא מוגדר). לדוגמה:
function f(input: boolean) { let a = 100; if (input) { // Still okay to reference 'a' let b = a + 1; return b; } // Error: 'b' doesn't exist here return b; }
אם b היה מוגדר ע"י var היה ניתן להשתמש בו גם מחוץ ל-if כי הוא היה קיים בתוך כל הפונקציה f.
- לא ניתן להשתמש במשתנה לפני שמגדירים אותו:
a++; // illegal to use 'a' before it's declared; let a;
עם זאת, עדיין ניתן להשתמש במשתנה בתוך פונקציה לפני שהגדרנו אותו, אבל אסור לקרוא לפונקציה הזו לפני שהגדרנו את המשתנה. לדוגמה:
function foo() { // okay to capture 'a' return a; } foo(); // illegal call 'foo' before 'a' is declared let a; foo(); //legal call
- לא ניתן להגדיר את אותו משתנה יותר מפעם אחת, גם אם פעם אחת משתמשים ב-var ופעם אחת ב-let. צריך לשים לב שאין התנגשות בין הפרמטרים שהפונקציה מקבלת לפרמטרים שמוגדרים בפונקציה. לדוגמה:
function f(x) { let x = 100; // error: x is a name of parameter in this function }
- בהמשך לסעיף הקודם, יש אפשרות להגדיר משתנה עם אותו שם של משתנה אחר בתנאי שעושים זאת בתוך בלוק פנימי יותר. דבר כזה נקרא shadowing. לדוגמה:
function f(condition, x) { if (condition) { let x = 100; return x; } return x; } f(false, 0); // returns '0' f(true, 0); // returns '100'
const
ב-TypeScript ניתן להגדיר קבועים ע"י שימוש ב-const.
const numDaysInWeek = 7;
אותם כללים של scoping של let חלים גם על const. ההבדל היחיד הוא שאי אפשר להכניס ל-const ערך מחדש.
צריך להבין שאי אפשר לשנות את המשתנה שמוגדר כ-const, אבל את המבנה הפנימי שלו ניתן לשנות. לדוגמה:
ניתן גם להשתמש ב-destructuring עם משתנים קיימים. לדוגמה כדי להחליף ערכים בין משתנים (swap) ניתן לכתוב:
ניתן גם להשתמש בצורה הזו כדי להגדיר סוגי משתנים לפונקציה:
צריך להבין שאי אפשר לשנות את המשתנה שמוגדר כ-const, אבל את המבנה הפנימי שלו ניתן לשנות. לדוגמה:
const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } // Error kitty = { name: "Danielle", numLives: numLivesForCat }; // all "okay" kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--;
destructuring
אפשרות נוספת שיש ב-ECMAScript 2015, וזמינה ב-TypeScript נקראת destructuring. וכדי להבין את זה הכי טוב להשתמש בדוגמאות.
בדרך הזו יוצרים שני משתנים first ו-second שכל אחד מקבל ערך מהמערך input לפי הסדר.
Array destructuring
let input = [1, 2]; let [first, second] = input; console.log(first); // outputs 1 console.log(second); // outputs 2
ניתן גם להשתמש ב-destructuring עם משתנים קיימים. לדוגמה כדי להחליף ערכים בין משתנים (swap) ניתן לכתוב:
[x, y] = [y, x];
ניתן גם להשתמש בצורה הזו כדי להגדיר סוגי משתנים לפונקציה:
function f([first, second]: [number, number]) { console.log(first); console.log(second); } f(input);
ניתן גם להגדיר משתנה שיקבל את כל שאר הערכים ע"י שימוש ב-"..."
let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // outputs 1 console.log(rest); // outputs [ 2, 3, 4 ]
או לקחת רק ערך אחד:
let [first] = [1, 2, 3, 4]; console.log(first); // outputs 1
או ערכים במיקומים מסוימים:
let [, second, , fourth] = [1, 2, 3, 4];
Object destructuring
שימוש דומה ניתן לעשות גם עם אובייקטים. לדוגמה:
let o = { a: "foo", b: 12, c: "bar" } let { a, b } = o;
בדוגמה זו נוצרים 2 משתנים a = o.a, b = o.b. כפי שרואים, אין חובה להשתמש ב-c אם אין צורך. מה שאנחנו עושים פה זה בעצם פירוק של האובייקט למשתנים. ולכן השמות של המשתנים צריכים להיות כמו שמות השדות של האובייקט. אם ננסה לעשות השמה למשתנים עם שמות אחרים נקבל שגיאה, למשל:
let {a, d} = o;// Error, d is not property of o
אם רוצים לשנות את שמות המשתנים שלא יהיו זהים לשמות שיש באובייקט ניתן לעשות זאת כך:
let { a: newName1, b: newName2 } = o;
כך ניצור שני משתנים חדשים newName1 = o.a, newName2 = o.b. צריך לשים לב שהנקודתיים לא מגדירים את סוג המשתנה כמו בדרך כלל.
אם רוצים גם להגדיר את סוג המשתנה נעשה זאת כך:
let { a: newName1, b: newName2 }: { a: string, b: number } = o;
אבל אין צורך להגדיר את סוג המשתנה כיון ש-newName1 יקבל את הסוג של a, ו-newName2 יקבל את הסוג של b.
בדומה למערכים, ניתן לעשות destructing גם למשתנים קיימים. לדוגמה:
({ a, b } = { a: "baz", b: 101 });
שימו לב שצריך לעטוף את זה בסוגריים כיון ש-javscript מפרש סוגריים מסולסלות כהתחלה של בלוק, ואם אנחנו רוצים לעשות destruct בצורה הזו צריך לעטוף אותו בסוגריים עגולות.
ניתן להשתמש ב"..." כדי להתייחס לשאר האברים באובייקט. לדוגמה:
let { a, ...rest} = o; console.log(rest.b);// print 12 console.log(rest.c);// print "bar"
ניתן גם לקבוע default value למקרה שיש צורך לדוגמה:
function keepWholeObject(myObj: { a: string, b?: number }) { let { a, b = 1001 } = myObj; }
בדוגמה הזו, אם יקראו לפונקציה עם אובייקט שאין לו b אז המשתנה b שבתוך הפונקציה יקבל ערך דיפולטיבי של 1001.
Function declarations
ישנם מספר שימושים אפשריים ב-destrcting בפונקציות.
1. הגדרת סוג הפרמטרים של הפונקציה:
type C = { a: string, b?: number } function f({ a, b }: C): void { // ... }
בדוגמה זו, הפרמטר a של הפונקציה יהיה מסוג string, והפרמטר b יהיה אופציונלי ומסוג number.
2. השמה של default values:
function f({ a, b } = { a: "", b: 0 }): void { // ... } f(); // ok, default to { a: "", b: 0 }
בדוגמה זו, הפרמטר a יהיה מסוג של המשתנה a שבצד ימין ויקבל ערך default של מחרוזת ריקה. הפרמטר b יהיה מסוג של המשתנה b שבצד ימין ויקבל ערך default של 0.
אם יש צורך שאחד המשתנים יהיה אופציונלי אז צריך לתת לו ערך default בהגדרת המשתנים (בצד שמאל) ולא בהגדרת ה-destructing (צד ימין). לדוגמה:
function f({ a, b = 0 } = { a: "" }): void { // ... } f({ a: "yes" }) // ok, default b = 0 f() // ok, default to { a: "" }, which then defaults b = 0 f({}) // error, 'a' is required if you supply an argument
שים לב שבשתי הדוגמאות האחרונות הפרמטרים a, b קיבלו ערכי default. אלא שבדוגמה הראשונה שני הפרמטרים קיבלו ערכי default בצד ימין, ואילו בדוגמה השנייה a קיבל ערך בצד ימין ו-b בצד שמאל. ההבדל ביניהם קצת מורכב ולכן ננסה להסביר אותו באופן מפורט:
בשני המקרים אם נקרא לפונקציה ללא פרמטרים אז ערכי ה-default ילקחו בחשבון.
בשני המקרים אם נקרא לפונקציה עם שני פרמטרים אז ערכי ה-default לא ילקחו בחשבון.
ההבדל הוא, אם ניתן לקרוא לפונקציה עם פרמטר אחד כך שרק b יקבל את ערך ה-default.
בדוגמה הראשונה לא ניתן לקרוא לפונקציה עם פרמטר אחד.
בדוגמה השנייה, ניתן לקרוא לפונקציה עם פרמטר אחד.
וזאת כיון ש-b מקבל ערך default בתוך הגדרת הפונקציה (צד שמאל) לכן ניתן לקרוא לפונקציה עם פרמטר אחד (a) ואז b יקבל את ערך ה-default.
Spread
האופרטור spread מאפשר הכנסה של ערכי מערך אחד למערך שני או הכנסה של ערכי אובייקט אחד לאובייקט שני.
דוגמה למערכים:let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5];// bothPlus will be [0,1,2,3,4,5]
דוגמה לאובייקטים:
let defaults = { food: "spicy", price: "$$", city: "Jerusalem" }; let search = { ...defaults, food: "sweet" };
השימוש של spread באובייקטים מורכב יותר מהשימוש במערכים. אם יש property שמופיע פעמיים, אז מתחילים משמאל לימין והימני יותר דורס את השמאלי. ולכן בדוגמה הזו התוצר הסופי יהיה:
search = { food: "sweet", price: "$$", city: "Jerusalem" };אם היינו כותבים:
let search = { food: "sweet", ...defaults};אז:
search = { food: "spicy", price: "$$", city: "Jerusalem" };
ישנה מגבלה של spread לגבי אובייקטים שמכילים פונקציות. הפונקציות לא עוברות לאובייקט החדש. לדוגמה:
class C { p = 12; m() { } } let c = new C(); let clone = { ...c }; clone.p; // ok clone.m(); // error!
Union
ב-TypeScript ניתן להגדיר משתנה שיכול להיות אחד ממספר סוגים. לדוגמה:
x = number | string; x = 3; // OK x = "whatever"; // OK
בדוגמה הזו, x יכול להיות number או string.
למשתנים מסוג union יש גישה רק לדברים שמשותפים לכל הסוגים, לדוגמה:
interface dog{ eat(); bite(); } interface bird{ eat(); fly(); } function getAnimal(): dog | bird { // ... } let myAnimal = getAnimal(); myAnimal.eat(); // okay myAnimal.fly(); // error
Intersection
ב-TypeScript ניתן להגדיר משתנה שיכיל מספר סוגים של משתנים אחרים. בשונה מ-union, אם רוצים להגדיר intersection צריך להגדיר את כל מה שיש בכל הסוגים שבהם השתמשנו. לדוגמה:
interface dog{ legs: number; tail: boolean; } interface bird{ legs: number; wings: boolean; } let Animal: dog & bird = { legs: 4, tail: true, wings: false }
אם לא הייתי מגדיר ב-Animal את wings לדוגמה, הייתי מקבל שגיאת קומפילציה.
Interfaces
ב-TypeScript
String literal types
ב-TypeScript ניתן לקבוע סוגי משתנים ע"י שימוש בערכים מסוימים ולא רק בסוגים כלליים. לדוגמה:
השימוש בזה יותר הגיוני ביחד עם Union. למשל:
בצורה זו ניתן לקבוע שלמשתנה מסוים יהיו רק כמה אפשרויות מסוימות ולא כל האפשרויות של אותו סוג כגון string.
let color: "Black"; color = "Black"; color = "White";// error!
השימוש בזה יותר הגיוני ביחד עם Union. למשל:
let color: "Black" | "White"; color = "Black"; color = "White";// OK
בצורה זו ניתן לקבוע שלמשתנה מסוים יהיו רק כמה אפשרויות מסוימות ולא כל האפשרויות של אותו סוג כגון string.
Type Aliases
ניתן לתת שם ל-type מסוים. האפשרות הזו בד"כ משלימה את ה-string literal types מהפיסקה הקודמת. אם נחזור לדוגמה הקודמת אז יהיה יותר יפה לכתוב אותה כך:
type optionalColors = "Black" | "White"; let color: optionalColors; color = "Black"; color = "White";
משתמשים במילה type כדי לתת שם לסוג מסוים שרוצים ליצור.
Polymorphic this types
יש אפשרות לגרום לכך ש-this יחזיר כל פעם משתנה מסוג שונה לפי הצורך. בדרך הזו אפשר לכתוב API מאוד נקי. כדי להבין את זה נשתמש בדוגמה:
class Animal{ saySomthing(){ //some code here return this; } } class Cat extends Animal{ eatMouse(){ //some code here return this; } } class Dog extends Animal{ eatCat(){ //some code here return this; } } let c = new Cat(); let d = new Dog(); c.saySomthing();// returns Cat d.saySomthing();// returns Dog
כפי שניתן לראות בדוגמה, כל מחלקה מחזירה ע"י this אובייקט שונה לפי סוג המחלקה שלה למרות שהפונקציה saySomething מומשה רק פעם אחת במחלקה הראשית.
Declaration merging
הקומפיילר של TypeScript יאחד כל מיני הגדרות אם יש להם שם זהה. לדוגמה אם במקום אחד בקוד כתוב:
interface Car { name: string; drive: () => void; }
ואילו במקום אחר כתוב:
interface Car { id: number; model: string; }
אז הקומפיילר יאחד את שתי ההגדרות ובפועל יהיה לנו משהו כזה:
interface Car { name: string; drive: () => void; id: number; model: string; }
לא כל דבר אפשר לאחד.
הדברים שאפשר לאחד הם:
- Interfaces
- Enums
- Namespaces
- Namespaces with class
- Namespaces with functions
- Namespaces with enums
הדברים שאי אפשר לאחד הם:
- Classes with classes
Type guards
ה-type guards מאפשרים בדיקה של סוג המשתנה. השימוש ב-type guards מאפשר לקומפיילר לצמצם את סוג המשתנה לסוג ספציפי. היתרון בזה הוא שהקומפיילר יכול למצוא יותר שגיאות על פי סוג המשתנה.
typeof
let x: string | number = 343; if (typeof x === 'string') { // x is a string } else { // x is a number }
ניתן להשתמש רק עם: "string", "number", "boolean", "symbol"
instanceof
class Cat { // some code here } class Dog { // some code here } let pet: Cat | Dog = new Cat(); if (pet instanceof Cat) { // pet is a Cat }
הבדיקה של Instaceof מתבצעת בין המשתנה לבין constructor function של מחלקה מסוימת.
user-defined type guard
הסינטקס של ה-return value של isAnimal (בדוגמה שלנו "a is Animal") זה מה שהופך את הפונקציה הזו ל-type guard. בעצם אנחנו בודקים את הפרמטר של הפונקציה מול הסוג שאנחנו רוצים. המילה is היא מילה שמורה ב-TypeScript.
interface Animal { numberOfLegs: number; } function isAnimal(a: any): a is Animal { return (<Animal>a).numberOfLegs !== undefined; } let d = new Dog(); if(isAnimal(d)) { //it's an animal }
הסינטקס של ה-return value של isAnimal (בדוגמה שלנו "a is Animal") זה מה שהופך את הפונקציה הזו ל-type guard. בעצם אנחנו בודקים את הפרמטר של הפונקציה מול הסוג שאנחנו רוצים. המילה is היא מילה שמורה ב-TypeScript.
Symbols
ה-symbol זה סוג חדש של משתנה בסיסי שהוכנס החל מ-ES2015.
ה-symbol הוא unique (כל symbol שונה משאר ה-symbols), ו-immutable (ברגע ש-symbol נוצר לא ניתן לשנות אותו).
ב-TypeScript יש תמיכה ב-symbol אבל צריך לשנות בקובץ tsconfig.json את השדה "target" ל-"es2015" כדי שיתמוך בסוג הזה.
שימוש ב-symbol כ-key באובייקט:
שימוש ב-symbol כשם של פונקציה במחלקה:
בקובץ שבו נשתמש בפונקציה הזו נצטרך לפני זה לייבא את MY_FUNC ואז נוכל להשתמש:
ישנה גם אפשרות לשנות את ההתנהגות הפנימית של שפת TypeScript ע"י שימוש ב-symbol. הצורך בזה הוא נדיר ולכן לא נכתוב עליו ורק נזכור שיש כזאת אפשרות וניתן למצוא אותה באתר הרשמי.
יש שני סוגים של-class decorator. הראשון לא משנה את ה-constructor function והשני משנה את ה-constructor function.
דוגמה לסוג הראשון:
ניתן לראות שמדובר ב-class decorator כיון שהפונקציה מקבלת רק פרמטר אחד והפרמטר מסוג Function. ניתן לראות שמדובר על הסוג הראשון (שלא משנה את ה-constructor function) מכך שהפונקציה מחזירה void.
הפרמטר שהפונקציה מקבל הוא ה-constructor של ה-class.
כדי להשתמש ב-decorator הזה פשוט נכתוב @ ואת שם ה-decorator לפני ה-class שעליו נפעל. לדוגמה:
אנחנו לא צריכים לתת ל-decorator את הפרמטר שהוא מקבל, TypeScript עושה את זה בצורה אוטומטית.
דוגמה לסוג השני:
Class decorator factory
ה-property decorator מקבל שני פרמטרים.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
גם ב-property decorator כדי להשתמש בו רק כותבים מעל ה-property שרוצים להפעיל עליו את ה-decorator את הסימן @ ולאחריו שם ה-decorator. אין צורך להעביר אליו שום פרמטר כי TypeScript עושה זאת באופן אוטומטי.
ה-property decorator נראה משהו כזה:
ה-parameter decorator מקבל שלושה פרמטרים.
הראשון והשני דומים ל-property decorator.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
הפרמטר השלישי הוא האינדקס של הפרמטר שעליו נפעיל את ה-decorator. האינדקס הכוונה למה מספר הפרמטר ברשימת הפרמטרים שיש לפונקציה שעליה נפעיל את הdecorator. הראשון יהיה מספר 0 השני יהיה 1 וכן הלאה.
ה-parameter decorator נראה משהו כזה:
ה-method decorator מקבל שלושה פרמטרים.
הראשון והשני דומים ל-property decorator.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
הפרמטר השלישי הוא ה-property descriptor של ה-method שעליו נפעיל את ה-decorator (ה-property descriptor זה פיצ'ר של JS שהתווסף ב-ES5 והוא בעצם אובייקט שמתאר property מסוים ואיך אפשר לעבוד איתו). אנחנו צריכים אותו כי הוא שולט על ה-method ובעזרתו נוכל לשלוט על ה-method ב-decorator שניצור. לדוגמה נוכל להפוך את ה-method ל-read only.
דוגמה ל-method decorator factory שקובע אם ה-method הוא read only או לא:
בדוגמה ניתן לראות הגדרה של method decorator factory ושימוש בו. אם ננסה לשנות את ההגדרה של הפונקציה functionName ב-run time נקבל error.
ה-symbol הוא unique (כל symbol שונה משאר ה-symbols), ו-immutable (ברגע ש-symbol נוצר לא ניתן לשנות אותו).
ב-TypeScript יש תמיכה ב-symbol אבל צריך לשנות בקובץ tsconfig.json את השדה "target" ל-"es2015" כדי שיתמוך בסוג הזה.
let mySymbol = Symbol('some_optionally_description');//the description is
// optionally and it can be string or a number
שימוש ב-symbol כ-key באובייקט:
let myObject = { [mySymbol]: 'value for my symbol' } console.log(myObject[mySymbol]);
שימוש ב-symbol כשם של פונקציה במחלקה:
export const MY_FUNC = Symbol(); class myClass { [MY_FUNC]():void { console.log('this is a print from function'); } }
בקובץ שבו נשתמש בפונקציה הזו נצטרך לפני זה לייבא את MY_FUNC ואז נוכל להשתמש:
import { MY_FUNC } from './file_path/file_name'; let x = new myClass(); x[MY_FUNC]();
ישנה גם אפשרות לשנות את ההתנהגות הפנימית של שפת TypeScript ע"י שימוש ב-symbol. הצורך בזה הוא נדיר ולכן לא נכתוב עליו ורק נזכור שיש כזאת אפשרות וניתן למצוא אותה באתר הרשמי.
Decorators
- ה-decorators הוא פיצ'ר עתידי שאמור להיות ב-JS אבל כבר זמין ב-TypeScript.
- הוא יכול להתקמפל ל-ES5 כבר כיום.
- ניתן לכתוב decorator ל-classes, methods, accessors, properties and parameters
- כדי להפעיל decorator פשוט כותבים @ ולאחריו את שם ה-decorator בשורה שמעל הדבר שעליו רוצים להפעיל את ה-decorator
- ניתן לדעת מה סוג ה-decorator לפי החתימה של הפונקציה (מהם הפרמטרים שהי מקבלת ומה סוג ההחזר שלה).
- אם ה-decorator ממומש בקובץ אחר צריך לייבא אותו לקובץ שבו רוצים להשתמש בו בצורה הבאה:
import {decoratorName} from 'pathToDecoratorFile';
- כדי להשתמש ב-decorators צריך להוסיף ל-tsconfig.json את הדבר הבא:
tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true } }
Class decorator
יש שני סוגים של-class decorator. הראשון לא משנה את ה-constructor function והשני משנה את ה-constructor function.
דוגמה לסוג הראשון:
function decoratorName(target: Function): void{ /*do something*/ }
ניתן לראות שמדובר ב-class decorator כיון שהפונקציה מקבלת רק פרמטר אחד והפרמטר מסוג Function. ניתן לראות שמדובר על הסוג הראשון (שלא משנה את ה-constructor function) מכך שהפונקציה מחזירה void.
הפרמטר שהפונקציה מקבל הוא ה-constructor של ה-class.
כדי להשתמש ב-decorator הזה פשוט נכתוב @ ואת שם ה-decorator לפני ה-class שעליו נפעל. לדוגמה:
@decoratorName class className{ }
אנחנו לא צריכים לתת ל-decorator את הפרמטר שהוא מקבל, TypeScript עושה את זה בצורה אוטומטית.
דוגמה לסוג השני:
function decoratorName<TFunction extends Function>(target: TFunction): TFunction { let newConstructor: Function = function () { //do something } newConstructor.prototype = Object.create(target.prototype); newConstructor.prototype.constructor = target; return <TFunction>newConstructor; }
Class decorator factory
ה-decorator factory הוא בעצם decorator דינאמי שמקבל ערך.
דוגמה ל-class decorator factory מהסוג הראשון:
בדוגמה זו ניתן לראות הגדרה של class decorator factory ושימוש בו. ניתן גם לראות שימוש בפרמטר שה-decorator קיבל (name).
דוגמה ל-class decorator factory מהסוג הראשון:
function decoratorName(name: string) { return function(target: Function): void{ /*do something*/ console.log('the name is : ${name}'); } } @decoratorName('someName') class className{ }
בדוגמה זו ניתן לראות הגדרה של class decorator factory ושימוש בו. ניתן גם לראות שימוש בפרמטר שה-decorator קיבל (name).
Property decorator
ה-property decorator מקבל שני פרמטרים.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
גם ב-property decorator כדי להשתמש בו רק כותבים מעל ה-property שרוצים להפעיל עליו את ה-decorator את הסימן @ ולאחריו שם ה-decorator. אין צורך להעביר אליו שום פרמטר כי TypeScript עושה זאת באופן אוטומטי.
ה-property decorator נראה משהו כזה:
function MyPrpertyDecorator (target: Object, PropertyName: string) { // do something }
Parameter decorator
ה-parameter decorator מקבל שלושה פרמטרים.
הראשון והשני דומים ל-property decorator.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
הפרמטר השלישי הוא האינדקס של הפרמטר שעליו נפעיל את ה-decorator. האינדקס הכוונה למה מספר הפרמטר ברשימת הפרמטרים שיש לפונקציה שעליה נפעיל את הdecorator. הראשון יהיה מספר 0 השני יהיה 1 וכן הלאה.
ה-parameter decorator נראה משהו כזה:
function MyParameterDecorator (target: Object, PropertyName: string, parameterIndex: number) { // do something }
Method decorator
ה-method decorator מקבל שלושה פרמטרים.
הראשון והשני דומים ל-property decorator.
הראשון הוא ה-constructor של ה-class שה-property שייך אליה, אם ה-property הוא static. אחרת הוא יהיה ה-prototype של ה-class שה-property שייך אליה.
הפרמטר השני זה שם ה-property שעליו ה-decorator פועל.
הפרמטר השלישי הוא ה-property descriptor של ה-method שעליו נפעיל את ה-decorator (ה-property descriptor זה פיצ'ר של JS שהתווסף ב-ES5 והוא בעצם אובייקט שמתאר property מסוים ואיך אפשר לעבוד איתו). אנחנו צריכים אותו כי הוא שולט על ה-method ובעזרתו נוכל לשלוט על ה-method ב-decorator שניצור. לדוגמה נוכל להפוך את ה-method ל-read only.
דוגמה ל-method decorator factory שקובע אם ה-method הוא read only או לא:
function readOnly (isReadOnly: boolean) { return function (target: Object, PropertyName: string, descriptor: PropertyDescriptor) { descriptor.writable = isReadOnly; } } @readOnly(true) functionName{ }
בדוגמה ניתן לראות הגדרה של method decorator factory ושימוש בו. אם ננסה לשנות את ההגדרה של הפונקציה functionName ב-run time נקבל error.
Callbacks
כדי לכתוב קוד אסינכרוני נשתמש בפונקציות callback. קוד אסינכרוני חשוב לטובת חווית המשתמש כדי שלא יווצר מצב שהתוכנה ניתקעת ומחכה לתשובה מאיזשהו מקום. ברגע שהתשובה מגיעה ניקרא לפונקציית ה-callback ונשתמש במה שהתקבל.
פונקציית callback היא פונקציה רגילה. עם זאת ישנה מוסכמה (בעיקר בקהילת ה-node) שפונקציות callback מקבלות 2 פרמטרים. הראשון הוא אובייקט error והשני הוא ה-data שהפונקציה צריכה להשתמש בו.
דוגמה לשימוש ב-callback בפונקציה אסינכרונית:
בדוגמה הזו שני ההדפסות ל-console יתבצעו לפני ההדפסה של פונקציית ה-callback. ולכן נקבל משהו כזה:פונקציית callback היא פונקציה רגילה. עם זאת ישנה מוסכמה (בעיקר בקהילת ה-node) שפונקציות callback מקבלות 2 פרמטרים. הראשון הוא אובייקט error והשני הוא ה-data שהפונקציה צריכה להשתמש בו.
דוגמה לשימוש ב-callback בפונקציה אסינכרונית:
interface someCallbackInterface { (err: Error, someValue: number): void; } function funcName(someData, callback:someCallbackInterface):void { returnValue = someAsyncFunction(someData); try{ if(returnValue != -1){ callback(null, returnValue); } else{ throw new Error('some error'); } } catch(error) { callback(error, null); } } function myCallbackFunction(err: Error, someValue: number):void { if(err) { console.log('the error is: ${err.message}'); } else { console.log('the return value is: ${someValue}'); } } console.log('before the call to async function'); funcName(someData, myCallbackFunction); console.log('after the call to async function');
before the call to async function after the call to async function
the return value is: 5
או במקרה של error:before the call to async function after the call to async function
the error is: some error
Promises
כדי להשתמש ב-promises צריך לקנפג את ה-TypeScript להתקמפל ל-ES2015. בקובץ tsconfig.json נשנה את השדה "target" ל-"es2015".
ה-constructor של ה-promise מקבל פונקציה. הפונקציה הזו מקבלת 2 פרמטרים.
הפרמטרים resolve ו-reject הם פונקציות שה-promise מבין.
אם הפעולה האסינכרונית בוצעה בהצלחה, קוראים לפונקציה resolve עם ה-data שאנחנו רוצים להעביר הלאה למי שישתמש בו.
אם הפעולה האסינכרונית נכשלה, קוראים לפונקציה reject עם הסיבה לכישלון.
הסוג של ה-promise, במקרה הזה <string>, הוא סוג ה-data שנשלח בפונקציה resolve במקרה שהפעולה האסינכרונית הצליחה.
צורה נפוצה יותר להגדרת promise נראית ככה:
הפעולה על תוצאת ה-promise מתבצעת על ידי 2 פונקציות: then ו-catch.
אם הפעולה האסינכרונית בוצעה בהצלחה, הפונקציה then תתבצע וה-data יהיה מה שנשלח ב-resolve. הפונקציה then גם היא מחזירה promise כך שבמקרה של כישלון נוכל לתפוס את הבעיה ע"י שימוש ב-catch. גם במקרה שהפעולה האסינכרונית הצליחה אבל נזרק exception מתוך ה-then גם אז הבעיה תיתפס בפונקציית ה-catch. ה-reason הוא הפרמטר שנשלח מהפונקציה reject.
ישנה דרך נוספת לטפל בכישלון של promise:
בצורה הזו, אם ה-promise נכשל הטיפול יתבצע בשורה השלישית ונחזיר 0 בדוגמה הזו.
עדיין יש ל-catch תפקיד, וזה לתפוס errors שעלולים להיות בתוך הפונקציה then.
ה-constructor של ה-promise מקבל פונקציה. הפונקציה הזו מקבלת 2 פרמטרים.
function funcName(resolve, reject) { //make some async calls if (success)
resolve(data);
else
reject(reason); } let p: Promise<string> = new Promise(funcName);
הפרמטרים resolve ו-reject הם פונקציות שה-promise מבין.
אם הפעולה האסינכרונית בוצעה בהצלחה, קוראים לפונקציה resolve עם ה-data שאנחנו רוצים להעביר הלאה למי שישתמש בו.
אם הפעולה האסינכרונית נכשלה, קוראים לפונקציה reject עם הסיבה לכישלון.
הסוג של ה-promise, במקרה הזה <string>, הוא סוג ה-data שנשלח בפונקציה resolve במקרה שהפעולה האסינכרונית הצליחה.
צורה נפוצה יותר להגדרת promise נראית ככה:
let p: Promise<string> = new Promise((resolve, reject)=> { //make some async calls if (success)
resolve(data);
else
reject(reason); });
הפעולה על תוצאת ה-promise מתבצעת על ידי 2 פונקציות: then ו-catch.
p.then(data => console.log(data))
.catch(reason => console.log(reason));
אם הפעולה האסינכרונית בוצעה בהצלחה, הפונקציה then תתבצע וה-data יהיה מה שנשלח ב-resolve. הפונקציה then גם היא מחזירה promise כך שבמקרה של כישלון נוכל לתפוס את הבעיה ע"י שימוש ב-catch. גם במקרה שהפעולה האסינכרונית הצליחה אבל נזרק exception מתוך ה-then גם אז הבעיה תיתפס בפונקציית ה-catch. ה-reason הוא הפרמטר שנשלח מהפונקציה reject.
ישנה דרך נוספת לטפל בכישלון של promise:
p.then(data => { console.log(data) }, reason => {return 0;}) .catch(reason => console.log(reason));
בצורה הזו, אם ה-promise נכשל הטיפול יתבצע בשורה השלישית ונחזיר 0 בדוגמה הזו.
עדיין יש ל-catch תפקיד, וזה לתפוס errors שעלולים להיות בתוך הפונקציה then.
async/await
המימוש של async/await נעשה תוך שימוש ביכולות שהוכנסו ב-ES2015 ולכן כדי להשתמש ב-async/await צריך לקנפג את ה-TypeScript להתקמפל ל-ES2015. בקובץ tsconfig.json נשנה את השדה "target" ל-"es2015".
כל פונקציה שמשתמשת ב-await חייבת להיות מוגדרת עם async לפניה. כל פונקציה שמוגדרת עם async/await מחזירה promise.
דוגמה:
מה שיקרה כאן זה כשנגיע לקריאה ל-getDataFromDb (זו פונקציה שמחזירה promise) נעצור עד שנקבל תשובה. שזה נשמע כמו קוד סינכרוני. אבל מצד שני הפונקציה someAsyncJob מוגדרת עם async שזה אומר שזו פונקציה אסינכרונית. ולכן הקוד הראשי שלנו הוא אסינכרוני והוא לא יתקע כשהוא מחכה לתשובה מה-DB. בתוך הפונקציה someAsyncJob יש קוד סינכרוני שמחכה עד שהוא מקבל תשובה מה-DB ורק אז ממשיך לרוץ.
כיון שפונקציה שמוגדרת עם async/await מחזירה promise ניתן לטפל בהחזר שלה בצורה דומה ל-promise ע"י catch כדלעיל.
כל פונקציה שמשתמשת ב-await חייבת להיות מוגדרת עם async לפניה. כל פונקציה שמוגדרת עם async/await מחזירה promise.
דוגמה:
async function someAsyncJob() { let result = await getDataFromDb (); console.log('the result is: ${result}'); } console.log('before the call to async function'); someAsyncJob() .catch(reason => console.log(reason)); console.log('after the call to async function');
מה שיקרה כאן זה כשנגיע לקריאה ל-getDataFromDb (זו פונקציה שמחזירה promise) נעצור עד שנקבל תשובה. שזה נשמע כמו קוד סינכרוני. אבל מצד שני הפונקציה someAsyncJob מוגדרת עם async שזה אומר שזו פונקציה אסינכרונית. ולכן הקוד הראשי שלנו הוא אסינכרוני והוא לא יתקע כשהוא מחכה לתשובה מה-DB. בתוך הפונקציה someAsyncJob יש קוד סינכרוני שמחכה עד שהוא מקבל תשובה מה-DB ורק אז ממשיך לרוץ.
כיון שפונקציה שמוגדרת עם async/await מחזירה promise ניתן לטפל בהחזר שלה בצורה דומה ל-promise ע"י catch כדלעיל.
איזה כללים מעולים לכתיבת קוד TypeScript.
השבמחקאתה יודע, אני זוכרת שראיתי בעבר מדריך דומה, אבל בלי תמונות
ויש משהו בהסבר הכתוב שמשנה את כל התמונה.
תודה רבה על כך.