Angular2 - ראוטינג




המקור להסברים כאן הוא מהאתר הרישמי: https://angular.io/docs/ts/latest/guide/router.html

ברוב המימושים של routing הדבר הראשון שצריך לעשות זה לקבוע את נקודת הבסיס לניווט בין דפי האתר. כדי להגדיר את הנקודה הזו משתמשים בתג <base> בתוך ה-index.html. אם ספריית app היא ספריית הבסיס באתר אז הגדרת נקודת הבסיס תהיה:


<base href="/">


הראוטר של אנגולר הוא לא חלק מה-core, אלא רכיב נפרד שאפשר להשתמש בו אבל לא חובה. הוא נמצא בספריה נפרדת באנגולר ואם רוצים להשתמש בו צריך לייבא אותו. כמו כן לא חובה לייבא את כולו. אפשר לייבא רק מה שרוצים מתוכו. לדוגמה:


import { RouterModule, Routes } from '@angular/router';

השימוש בראוטר הוא בצורת singleton. 
כאשר ה-URL משתנה הראוטר מחפש את הנתיב (route) המתאים כדי לקבוע איזה component להציג. בהתחלה אין לראוטר נתיבים בכלל עד שמגדירים לו מהם הנתיבים שלו.

הגדרת routes

כדי לקנפג את ה-router יוצרים מערך של routes. כל route מוגדר מ-path ו-component.
ה-path זה חלק מה-URL. וה-component הוא הרכיב שיוצג. 
דוגמה למערך של routes:


const appRoutes: Routes = [
  { path: 'first-page', component: FirstPageComponent },
  { path: 'second-page', component: SecondPageComponent },
];



Router outlet

המיקום שבו הראוטר יציג את ה-component הנבחר יהיה לאחר התג:
 <router-outlet></router-outlet> 

שנמצא ב-HTML של ה-host view של האתר.


Router links

עד כאן יש לנו טבלת routes ומיקום שאליו הראוטר מרנדר את ה-component. ה-URL יכול להגיע ישירות משורת הכתובת של ה-browser אבל רוב הפעמים הניווט באתר מתבצע ע"י לחיצות של המשתמש על לינקים.
נשתמש ב-template הבא כדי להסביר:


template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

ה-routerLink, מאפשר לראוטר לשלוט על האלמנטים האלה ומציין לאיזה path ללכת כשלוחצים על הלינק.
ה-routerLinkActive, משייך את האלמנט הנוכחי ל-class שנקרא active ברגע שה-routerLink הופך להיות active, או במילים אחרות ברגע שלוחצים על הלינק הצבע של האלמנט משתנה כך שניתן לראות על מה לחצנו.


Router state

כל פעם שהראוטר מבצע route הוא בונה עץ של אובייקטים מסוג ActivatedRoute שמייצג את המצב הנוכחי של הראוטר. ניתן לגשת לסטאטוס של הראוטר מכל מקום בקוד ע"י ה-router service וה-routerState property. לכל אובייקט ActivatedRoute שב-RouterState יש פונקציות שמאפשרות גישה ל-route הבא, הקודם וגם ל-route צדדי.

ה-component הראשי - AppComponent 

ה-AppComponent הוא השלד של האפליקציה. יהיו בו חלקים קבועים לכל האתר וחלקים שמשתנים לפי הדף המוצג.
בד"כ יש לו title וגם navbar ו-footer שיופיעו בכל הדפים באתר. ויש את החלק שמשתנה לפי הדף המוצג. החלק הזה מגיע מה-router ומסומן ב-<router-outlet></router-outlet>.
דוגמה של template של AppComponent:

app.component.ts

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/first-page" routerLinkActive="active">First Page</a>
    <a routerLink="/second-page" routerLinkActive="active">Second Page</a>
  </nav>
  <router-outlet></router-outlet>
`

ב-template הזה ניתן לראות את ה-directive שנקרא routerLinkActive. מה שיש בתוך הגרשיים זה מחלקה של CSS. ה-router יוסיף את המחלקה הזו ברגע שהלינק הזה מופעל ויסיר את המחלקה הזו כשהלינק לא מופעל.
ה-router מוסיף את המחלקה לפי המצב ב-RouterState. וכיון שה-RouterState בונה עץ של אובייקטים (router tree) יתכן שיהיו אבא ובן ששניהם מופעלים. 

אם לא רוצים שלשניהם תתווסף המחלקה אז צריך לעשות binding בין [routerLinkActiveOptions] ל-{ exact: true }. ואז רק הדף שה-URL המדויק שלו פעיל יסומן כפעיל.

מימוש הראוטר

ישנם שלוש אפשרויות בהגדרת הראוטר:

  1. כל הראוטר מוגדר בתוך ה-module הראשי (שנקרא בד"כ AppModule)
  2. כל הראוטר מוגדר ב-module נפרד משלו (שנקרא בד"כ AppRoutingModule)
  3. הראוטר מפוצל ומוגדר במספר modules. כל feature module מגדיר חלק מהראוטר לפי מה שהוא צריך
האפשרות הראשונה מתאימה לאתרים שדורשים ראוטינג פשוט.
האפשרות השניה מתאימה לאתרים יותר מורכבים שדורשים ראוטינג יותר מורכב ושימוש משמעותי יותר באפשרויות שהראוטר של Angular מציע.
האפשרות השלישית גם היא נועדה לאתרים שדורשים ראוטינג מורכב ויש לה כמה יתרונות.
נסביר כל אפשרות בצורה מפורטת.

אפשרות #1 - הראוטר מוגדר בתוך ה-module הראשי

כאשר מכניסים את ה-RouterModule ל-module הראשי (AppModule) אז ה-router זמין בכל האפליקציה.

בדוגמה הבאה מגדירים ארבעה נתיבים. מקנפגים את הראוטר ע"י שימוש בפונקציה-RouterModule.forRoot וההחזר של הפונקציה נכנס למערך ה-imports של ה-AppModule.

app.module.ts

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  { path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
    // other imports here
  ],
  ...
})
export class AppModule { }


  • כל route ממפה URL ל-component. הראוטר בונה את ה-URL הסופי בשבילנו ומאפשר לנו להשתמש בכתובות מדויקות ויחסיות כדי לנווט בין views. 
  • ה-"id:" שב-route השני מייצג העברת פרמטר לדוגמה hero/3. ה-3 הוא הערך שנשלח בפרמטר id וה-component שישתמש בו הוא HeroDetailComponent כדי להציג את הגיבור שה-id שלו הוא 3.
  • ב-route השלישי יש property שנקרא data. ה-property הזה נועד לשמור מידע שנצרך ל-route השלישי. ניתן להשתמש ב-data property בכל route לפי הצורך כדי לשמור בו מידע סטאטי (read only).
  • ב-route הרביעי יש path ששווה למחרוזת ריקה, וזה מה שמגדיר את ה-default route של האתר. לשם יפנה האתר ברגע שה-path ריק שזה בד"כ המצב בהתחלה. בדוגמה הזו ה-default route מפנה ל-heroes/ ולכן הוא יציג את HerosListComponent.
  • ב-route הרביעי משתמשים ב-redirectTo. כשמשתמשים ב-redirectTo חייבים להגדיר את ה-property שנקרא pathMatch. ה-property הזה קובע באיזה URL יתבצע redirect. במקרה שלנו הוא מוגדר כ-'full', מה שאומר שרק כשקטע ה-URL שמעבר לבסיס (localhost:3000) יהיה שווה ל-' ', רק אז יתבצע redirect ל-heroes. ה-pathMatch יכול גם לקבל 'prefix', והמשמעות היא שאם קטע ה-URL שמעבר לבסיס מתחיל במה שכתוב ב-path כבר אז יתבצע redirect. במקרה הנ"ל אם נשתמש ב-'prefix' כל URL יתאים ותמיד יתבצע redirect, ואת זה כמובן אנחנו לא רוצים.
  • ב-path האחרון יש "**" שזה wildcard. הראוטר ישתמש ב-route הזה במקרה שה-URL לא תאם לאף אחד משאר האפשרויות. במקרה הזה ניתן להציג דף של שגיאת 404 או להפנות לדף אחר כגון דף הבית.
  • יש חשיבות לסדר של ה-routes. ה-router משתמש ב-route הראשון שמתאים, ולכן צריך למקם route שיותר מפורט לפני route כללי. זו הסיבה שבדוגמה לעיל ה-wildcard route ממוקם אחרון כיון שהוא מתאים לכל URL וצריך שהוא יבחר רק אם כל שאר האפשרויות לא מתאימות. אם נמקם אותו ראשון הוא יבחר תמיד.
אפשרות #2 - כל הראוטר מוגדר ב-module נפרד משלו

מימוש ה-router כמו באפשרות #1 מתאים לאפליקציות פשוטות. באפליקציות מורכבות יש צורך ב-Module נפרד שיטפל ב-routing. השימוש ב-router module נפרד יפריד את ענייני ה-routing מענייני האפליקציה. בנוסף אם יש צורך לשנות routing בשביל בדיקות צריך להחליף רק את ה-module הזה בלבד. הקובץ app-routing.module.ts יראה כך:

app-routing.module.ts

import { NgModule }              from '@angular/core';
import { RouterModule, Routes }  from '@angular/router';
import { FirstComponent }   from './first.component';
import { SecondComponent }     from './second.component';
import { PageNotFoundComponent } from './not-found.component';
const appRoutes: Routes = [
  { path: 'first-component',  component: FirstComponent },
  { path: 'second-component', component: SecondComponent },
  { path: '',   redirectTo: '/first-component', pathMatch: 'full' },  
  { path: '**', component: PageNotFoundComponent }
];
@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}

וה-module הראשי app.module.ts יראה כך:

app.module.ts

import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { AppComponent }          from './app.component';
import { AppRoutingModule }      from './app-routing.module';
import { FirstComponent }        from './first.component';
import { SecondComponent }       from './second.component';
import { PageNotFoundComponent } from './not-found.component';
@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    FirstComponent ,
    SecondComponent ,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

שימו לב שבקובץ app-routing.module.ts אנחנו עושים export ל-RouterModule של Angular, ובקובץ app.module.ts אנחנו עושים import ל-AppRoutingModule שלנו. בדרך הזו לכל ה-components שמוגדרים ב-AppModule תהיה גישה ל-directives של הראוטר של Angular. כמו לדוגמה RouterLink ו-RouterOutlet.
שימו לב שבאפשרות #2 ה-imports שב-app.module.ts השתנה ובמקום RouterModule.forRoot שהיה בו, יש בו עכשיו AppRoutingModule.

אפשרות #3 - הראוטר מפוצל ומוגדר במספר modules, כל feature module מגדיר חלק מהראוטר

את ה-module של הראוטר (בדוגמה הזו heroes-routing.module.ts) כדאי לשים באותה ספריה של ה-module שהוא משתייך אליו (heroes.module.ts).
בכל module שמגדיר חלק מהראוטר יהיה מערך של routes שרלוונטים לגבי ה-module הנוכחי. את המערך הזה נכניס לתוך ה-imports. אבל בשונה משני האפשרויות הראשונות, הפעם נשתמש בפונקציה forChild (ולא forRoot). השימוש ב-forRoot יהיה רק בראוטר שמוגדר במודול הראשי (אפשרות #1) או במודול של ראוטר ראשי (אפשרות #2).
בנוסף צריך להכניס למערך ה-exports את ה-RouterModule ולייצא את המודול של הראוטר הנוכחי (HeroRoutingModule).


heroes-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';
const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];
@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

לאחר מכן צריך לייבא את הראוטר החדש (heroes-routing.module.ts) לתוך המודול שאליו הוא שייך (heroes.module.ts). נוסיף שורה של import בתחילת הקובץ ונוסיף אותו גם לתוך ה-imports.


heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';
import { HeroListComponent }    from './hero-list.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService } from './hero.service';
import { HeroRoutingModule } from './heroes-routing.module';
@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

את המודול heroes.module.ts מייבאים לתוך המודול הראשי, AppModule, וכך המודול וכל הראוטינג שלו יהיו זמינים בכל האפליקציה. גם כאן היבוא מתבצע בשני תוספות, א - הוספת שורת import בתחילת הקובץ, שמייבא את המודול מתוך הקובץ בו הוא מוגדר. ב - הוספה של המודול לתוך מערך ה-imports.


כללי כתיבה ב-TypeScript



  • בעמוד הזה תמצאו כללים בכתיבת קוד 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
ניתן להוסיף משתנים למערך שיהיו אחד מכל הסוגים שצויינו. בדוגמה שלנו ניתן להוסיף משתנים רק מסוג string או number :

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 קיים רק בתוך אותו בלוק שבו הוא מוגדר (בשונה מ-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'

בדוגמה שלעיל, כיון שה-x השני מוגדר בתוך בלוק פנימי יותר (בתוך if במקרה הזה) אז זו כתיבה חוקית. בבלוק הפנימי x ייצג את ה-x הפנימי. מחוץ ל-if ה-x ייצג את הפרמטר x שהפונקציה קיבלה כ-input. זו דוגמה פשוטה ל-shadowing. כפי שניתן לראות, צורת כתיבה כזו אמנם חוקית אבל לא קשה להבנה וכדאי להימנע ממנה.


const

ב-TypeScript ניתן להגדיר קבועים ע"י שימוש ב-const.


const numDaysInWeek = 7;

אותם כללים של scoping של let חלים גם על const. ההבדל היחיד הוא שאי אפשר להכניס ל-const ערך מחדש. 
צריך להבין שאי אפשר לשנות את המשתנה שמוגדר כ-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. וכדי להבין את זה הכי טוב להשתמש בדוגמאות.
Array destructuring


let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

בדרך הזו יוצרים שני משתנים first ו-second שכל אחד מקבל ערך מהמערך input לפי הסדר. 
ניתן גם להשתמש ב-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 ניתן לקבוע סוגי משתנים ע"י שימוש בערכים מסוימים ולא רק בסוגים כלליים. לדוגמה:

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


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" כדי שיתמוך בסוג הזה.


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 מהסוג הראשון:

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 בפונקציה אסינכרונית:


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');

בדוגמה הזו שני ההדפסות ל-console יתבצעו לפני ההדפסה של פונקציית ה-callback. ולכן נקבל משהו כזה:
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 פרמטרים.


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.
דוגמה:


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 כדלעיל.