TypeScript jest rozszerzonym JavaScriptem, transpilowanym (tłumaczenie kodu źródłowego w inny kod źródłowy) do JavaScript. Zawiera to samo co JavaScript i poprawny kod JavaScript jest również poprawny w TypeScript. Jednak bezsensu byłoby wykorzystywać transpilator i pisać kod tak samo. Jakie są zatem zalety TypeScript i jak z nich skorzystać?

TypeScript

Typescript (oficjalna strona) jest darmowym i open-sourceowym językiem programowania stworzonym i rozwijanym przez Microsoft. Język został stworzony do tworzenia dużych aplikacji internetowych. Dzięki temu, że język jest transpilowany można wychwycić więcej błędów na tym etapie. Inaczej niż w JavaScript, gdzie o błędach dowiesz się dopiero w momencie uruchomienia kodu. Nad rozwojem TypeScript czuwał Anders Hejlsberg, główny architekt C#, twórca Delphi i TurboPascala. Pierwsza publiczna wersja TypeScript 0.8 została wydana w 2012. Wtedy był zapowiedzią tego czego należy się spodziewać w ECMAScript2015. Od tego czasu TypeScript jest ciągle rozwijany i w kwietniu 2017 najnowszą wersją jest 2.2.

Jeśli chcesz na szybko zobaczyć jak działa, możesz zacząć od onlineowego narzędzia Playground. Natomiast jeśli chcesz zainstalować TypeScript i zacząć naukę zalecam zainstalować kompilator przez npm (Node.js Package Manager):

npm install -g typescript

Sprawdź czy wszystko działa poprawnie odpalając w konsoli tsc -v

tsc -v
Version 2.2.2

Pierwszy plik TypeScript

Do pisania w TypeScript bardzo poręczne jest IDE Visual Studio Code.

Stwórz nowy plik main.ts z przykładową zawartością:

let a:number = 2;

Transpiluj TypeScript na JavaScript:

tsc main.ts

Możesz też włączyć watchowanie zmian, dzięki transpilacja będzie następowała automatycznie z każdym zapisem pliku.

tsc main.ts --watch

Wynikiem będzie plik main.js. Powyższy przykład zostatnie zamieniony na JavaScript w ECMAScript 3:

var a = 2;

Let i const zmiast var

W nowym ECMAScript, a więc i w TypeScript dodane zostały dwa nowe sposoby definiowania zmiennych: let do definiowania zmiennych i const do definiowania stałych. I zalecenie jest proste: zawsze używać tych nowych sposobów zamiast var. Czemu? Ponieważ let i const mają poprawniejszy, bardziej zawężony zakres co prowadzi do mniejszej ilości błędów.

TypeScript – Podstawowe Typy

Boolean

Zwykła wartość prawda lub fałsz:

let isActive : boolean = true;

String i Template String

String – ciąg znaków:

let name : string = 'Jan';

Template String – zapisuje się je w odwrotnym apostrofie, mogą być zapisane w wielu liniach i mogą zawierać dodatkowe wyrażenia ${ wyrażenie }

let name : string = 'Jan';
let hello: string = `Cześć, nazywam się ${ name }`.

Liczba (number)

Tak jak w JavaScript, wszystkie liczby są zmiennoprzecinkowe i posiadają jeden zbiorczy typ number. Przykłady:

let decimal: number = 61;
let hex: number = 0xf00c;
let binary: number = 0b1101; //ECMAScript 2015
let octal: number = 0o633; //ECMAScript 2015

Tablice (Arrays)

Istnieją dwie notacje zapisu tablic. Używając nawiasów kwadratowych po typie zmiennej:

let cyfry: number[] = [1, 2, 3];

oraz używając notacji generycznej tablicy:

let cyfry: Array = [1, 2, 3];

Uporządkowana para (Tuple)

Tuple to taka w tablica w której zdefiniowane są typy konkretnych indeksów:

let t: [string, number];
t = ['Jan', 24]; // Poprawne

t = [24, 'Jan']; // Błąd

Spis (Enum)

Enum służy do nadawania bardziej przyjaznych i więcej mówiących nazw do liczb. Np. kody przycisków:

enum KEYS {
LEFT_ARROW = 37,
UP_ARROW = 38,
RIGHT_ARROW = 39,
DOWN_ARROW = 40
}

//odwolanie:
if (code === KEYS.LEFT_ARROW) {}

Dowolny typ (Any)

Kiedy nie wiesz jakiego typu będzie zmienna, gdyż może np. pochodzić z dynamicznie ładowanego contentu możesz zadeklarować typ any. Czyli tak jak w zwykłym JavaScripcie, tylko w TypeScript świadomie.

let nieznany : any = 4;
//albo nie, nieznany niech przechowuje string
nieznany = 'Jan';

Void

Void to odwrotność any, czyli nieposiadanie rzadnego typu. Używany kiedy funkcja nie zwraca rzadnej wartości. W przypadku zmiennych używanie nie ma sensu, gdyż jedyne możliwe wartości jakie można by przypisać do undefined lub null.

function close(): void {
    window.close();
}

Null lub undefined

Jak w JavaScript – symboliczne odzwierciedlenie braku wartości.

Never

Never informuje, że wartość nigdy się nie pojawi np. kiedy funkcja zawsze rzuca wyjątek lub nigdy nie skończy skończy.

function error(message: string): never {
    throw new Error(message);
}
//lub
function wiecznaPetla(): never {
    while (1) {
    }
}

Potwierdzenia typu (Type assertions)

Czasem może wystąpić sytuacja kiedy możesz lepiej wiedzieć co się znajduje pod zmienną niż kompilator, możesz wtedy mu pomóc deklarując typ:

let text: any = "Programowanie jest super!";
let len: number = (<string>text).length;
//lub drugi zapis:
let len: number = (text as string).length;

Interfejsy (Interfaces)

Jedną z głównych cech języka jest sprawdzanie typów zmiennych. Interfejsy są mechanizmem nazywania złożonych typów zmiennych i używania ich tak samo jak typów podstawowych. W poniższym przykładzie kompilator sprawdzi, czy objekt przekazany do funkcji showAlert posiada pole text:

interface Message {
    text: string;
}
function showAlert(msg: Message) {
    console.log(msg.text);
}
showAlert({text : 'No data!'});

Klasy (Classes)

W „zwykłym” JavaScript wykorzystuje się programowanie prototypowe w celu stworzenia reużywalnych komponentów. TypeScript (ECMAScript 2015) wprowadza bardziej komfortowe i ituicyjne programowanie objektowe wykorzystując klasy, znane z innych języków zorientowanych obiektowo.

Przykład definicji klasy i obiektu tej klasy:

class Employee {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    fire() {
        alert('You are fired, ' + this.name);
    }
}

let employee = new Employee("Jan");
employee.fire();

Dziedziczenie (Inheritance)

Jedna z fundamentalnych cech programowania obiektowego to rozszerzanie istniejących klas, żeby stworzyć nowe klasy. W tym języku wygląda nastepująco:

class Vehicle {
    speed: number;
    isMoving : boolean = false;
    constructor(speed: number) { this.speed = speed; }
    move() {
        this.isMoving = true;
    }
}

class Car extends Vehicle {
    constructor(speed: number) { super(speed); }
    move() {
        console.log("Car is moving.");
        super.move();
    }
}

Hermetyzacja (Enkapsulacja) – Public, Private i Protected

Hermetyzacja to kolejna z głównych cech programowania obiektowego. Polega na ukrywaniu pól i metod klasy, aby były widoczne tylko przez metody z tej samej klasy. Domyślnym poziomem widoczności jest public, czyli swobodny i nieograniczony dostęp.

Public

Poniższy kod:

class Car {
  model : string;
  constructor(model : string) {
    this.model = model; //OK
  }
}

new Car('126p').model; //OK

Jest tożsamy z:

class Car {
  public model : string;
  constructor(model : string) {
    this.model = model; //OK
  }
}

new Car('126p').model; //OK

Private

Pole z modyfikatorem private jest widoczne tylko z wewnątrz klasy:

class Car {
  private model : string;
  constructor(model : string) {
    this.model = model; //OK
  }
  public getModel() {
    return this.model;
  }
}

let car = new Car('126p');
car.getModel(); //OK
car.model; //Error, 'model' is private;

Protected

Modyfikator protected zachowuje się bardzo podobnie do private, ale jedną różnicą. Umożliwia dostęp z klasy rozszerzającej (dziedziczącej) po klasie, która zawiera ten modyfikator.

class Vehicle {
  protected brand : string;
  constructor(brand : string) {
    this.brand = brand;
  }
}

class Car extends Vehicle {
  private model : string;
  constructor(brand : string, model : string) {
    super(brand);
    this.model = model;
  }
  public getModel() {
    return this.model;
  }
  public getBrand() {
    return this.brand; //OK
  }
}

let car = new Car('Fiat','126p');
car.getBrand(); //OK
car.brand; //Error

Modyfikator readonly

Modyfikator readonly zamienia pola w tylko do odczytu. Wartość musi być ustawiona od razu przy deklaracji albo przez konstruktor.

class Car {
  readonly model : string;
  constructor(model : string) {
    this.model = model; //OK
  }
  public getModel() {
    return this.model;
  }
}

let car = new Car('126p');
car.getModel(); //OK
car.model = 'Syrenka'; //Error, 'model' is readonly;

Deklaracja pól prosto z konstruktora

Powyższy przykład można zapisać krócej i zadeklarować pole tylko w konstuktorze:

class Car {
  constructor(readonly model : string) { }
  public getModel() {
    return this.model;
  }
}

let car = new Car('126p');
car.getModel(); //OK
car.model = 'Syrenka'; //Error, 'model' is readonly;

Settery i gettery

Set i Get umożliwiają stworzenie interfejsu do prywatnego pola, umożliwia to kontrolę nad tym jaka wartość zostanie przypisana do pola, albo kto ją może ustawić, czy pobrać.

class Car {
  private _model : string;

  get model() : string {
    return this._model;
  }

  set model(newModel : string) {
    this._model = newModel.toLowerCase();
  }
}

let car = new Car();
car.model = '126P'; //duże P

console.log( car.model ); //wypisze '126p' z małym 'p'

Jeśli kompilator wyświetla: Accessors are only available when targeting ECMAScript 5 and higher. to użyj flagi target ustawionej na ES5.

tsc main.ts --target ES5

Modyfikator Static

Domyślnie pola klasy są dostępne dopiero po powołaniu obiektu do życia danej klasy (operator: new). Modyfikator static sprawia, że pole będzie istniało w jednej instancji, przez cały czas działania programu, czyli będzie dostępne zawsze, nawet przed stworzeniem obiektu. Można się wtedy do niego odwołać przez nazwę klasy.

class Thing {
  static pi : number = 3.14159;
}

console.log( Circle.pi );

Klasy abstrakcyjne (Abstract Classes)

Klasy abstrakcyjne to klasy „szablony”, które nie mogą być bezpośrednio powołane do życia, a tylko inne klasy mogą z nich dziedziczyć. W odróżnieniu od interfejsu klasy abstrakcyjne mogą zawierać implementację metod. Metody, które nie mają zdefiniowanej implementacji (ciała metody) muszą również być oznaczone jako abstrakcyjne.

abstract class Vehicle {
  protected brand : string;
  constructor(brand : string) {
    this.brand = brand;
  }
  public move() : void {
    console.log('Vehicle moved.');
  }
  abstract public start() : void; //musi być zaimplementowana w klasie, która będzie dziedziczyć po Vehicle
  abstract public stop() : void;  //musi być zaimplementowana w klasie, która będzie dziedziczyć po Vehicle
}

class Car extends Vehicle {
  private model : string;
  constructor(brand : string, model : string) {
    super(brand);
    this.model = model;
  }
  public start() : void {
    console.log('Car started.');
  }
  public stop() : void {
    console.log('Car stopped.');
  }
}

let car = new Car('Fiat','126p');
car.move(); //Vehicle moved.
car.start(); //Car started.
car.stop(); //Car stopped.

Funkcje (Functions)

W JavaScript funkcje są fundamentalnym składnikiem zastępującym klasy, enklapsulację, moduły. W TypeScript istnieją wszystkie te mechanizmy, więc nie ma potrzeby wykorzystywać funkcji do tych celów. Istnieją również dodatkowe mechanizmy, który JavaScript nie posiada.

Silne typowanie, parametrów funkcji i zwracanego typu:

function add(a: number, b: number): number {
    return a + b;
}

Opcjonalne parametry funkcji

Jeśli chcesz, żeby patametr funkcji był opcjonalny przy wywoływaniu funkcji, to musisz po jego nazwie dodać znak zapytania:

function printName(firstName: string, lastName?: string): void {
    if (lastName) {
      console.log( firstName + ' ' + lastName );
    } else {
      console.log( firstName );
    }
}

Domyślna wartość parametru funkcji

Jeśli chcesz, żeby patametr funkcji miał domyślną wartość, to możesz ją podać po znaku równości:

function printName(firstName: string = 'Jan', lastName?: string): void {
    if (lastName) {
      console.log( firstName + ' ' + lastName );
    } else {
      console.log( firstName );
    }
}
printName(undefined,'Nowak'); // wypisze Jan Nowak

Więcej parametrów funkcji

Jeśli chcesz, żeby funkcja przyjmowała zmienną ilość parametrów, ale nie wiesz ile dokładnie, to możesz użyć znaku wielokropka (…).

function printNames(firstName: string = 'Jan', ...restNames[]: string): void {
    console.log( firstName + " " + restNames.join(" ") );
}
printNames('Jan','Roman','Wacław','Waldemar');

Funkcje strzałkowe (Arrow functions)

Funkcje strzałkowe (arrow functions) to nowość z ES6. W odróżnieniu od zwykłych funkcji, deklaruje się je według wzorca:

(parametry) => ciało funkcji

W odróżnieniu od zwykłych funkcji deklarowanych słowem kluczowym function, funkcje strzałkowe nie posiadają własnej wartości this. This jest zawsze dziedziczone z nadrzędnego zakresu, przez co często nie trzeba tworzyć osobnej zmiennej przechowującej this.

class Thing {
  private creationDate : number;
  constructor() {
    this.creationDate = Date.now();

    setInterval( () => {
      console.log( 'Od utworzenia obiektu mineło: ' + Math.floor((Date.now() - this.creationDate)/1000) + ' sekund.');
    }, 1000);

  }
}
new Thing();

A gdyby użyć zwykłej deklaracji funkcji, to this.creationDate było by nieznane i trzeba by było dodać dodatkową zmienną self:

class Thing {
  private creationDate : number;
  constructor() {
    this.creationDate = Date.now();
    let self = this;

    setInterval( function() {
      console.log( 'Od utworzenia obiektu minęło: ' + Math.floor((Date.now() - self.creationDate)/1000) + ' sekund.');
    }, 1000);

  }
}
new Thing();

Inny przykład z użyciem metody reduce. Metoda Reduce zwraca sumę liczb w tablicy.

W JavaScript:

var numbers = [5, 8, 12, 5];

var sum = numbers.reduce(function(total, item) {
    return total + item;
});

console.log( 'Suma: ' + sum ); //Suma: 30

W TypeScript z Arrow function:

let numbers = [5, 8, 12, 5];

let sum = numbers.reduce( (total, item) => total + item );

console.log( 'Suma: ' + sum ); //Suma: 30

Programowanie generyczne/uogólnione (ang. generics)

Programowanie generyczne jest możliwe w wielu językach programowania np. C++, Java, C#. Ten paradygmat programowania pozwala na pisanie kodu bez wcześniejszego sprecyzowania typów danych, na których kod będzie pracował. Zmienne generyczne w TypeScript zapisuje się znakach mniejszości i większości np. <T>. Dzięki takiemu podejściu dopiero w trakcie powoływania obiektu do życia możesz zdecydować na jakich typach danych będzie ten objekt pracował. Kod staje się przez to bardziej reużywalny. W poniższym przykładzie jest stworzona generyczna klasa Alert, która jest powołana do życia dwa razy. Raz z typem Message i drugi raz SimpleMessage.

Przykład bez programowania generycznego:

class Alert{
    print ( message : Object) {
        if (message['type']) {
            console.log( message['type'] + ': ' + message['text'] );
        } else {
            console.log( message['text'] );
        }
    }
}
let alert1 = new Alert();
let alert2 = new Alert();

alert1.print( { type : 'Error', text: 'Some custom message'} );
alert2.print( { text: 'Some other message'} );

W powyższym przykładzie powołujemy do życia klasę Alert i przekazujemy obiekty do metody print. Jednak kod skompiluje się z każdym przekazanym obiektem i o ewentualnym błędzie, czy literówce dowiesz się dopiero w trakcie wykonywania programu.

A teraz jeszcze raz ten sam kod, ale zapisany w sposób generyczny:

interface Message {
    type: string;
    text: string;
}

interface SimpleMessage {
    text: string;
}

class Alert {
    print ( message : T) {
        if (message['type']) {
            console.log( message['type'] + ': ' + message['text'] );
        } else {
            console.log( message['text'] );
        }
    }
}

let alert1 = new Alert();
let alert2 = new Alert();

alert1.print( { type : 'Error', text: 'Some custom message'} );
alert2.print( { text: 'Some other message'} );

Teraz gdy zrobisz literówkę np. w linii (type2 zamiast type):

alert1.print( { type2 : 'Error', text: 'Some custom message'} );

To analiza statyczna kodu od razu pokaże błąd, który będziesz mógl szybko wyeliminować.

[ts]
Argument of type '{ type2: string; text: string; }' is not assignable to parameter of type 'Message'.
Object literal may only specify known properties, and 'type2' does not exist in type 'Message'.

Uruchamianie TypeScript w przeglądarce – Browserify

Może nie na samym początku nauki TypeScript, ale w pewnym momencie (kiedy zaczniesz rozbijać kod na moduły) kod przetranspilowany do JavaScript przestanie się uruchamiać w przeglądarce. Wtedy konieczne staje się skorzystanie z kolejnego narzędzia Browserify (oficjalna strona). Browserify połączy wszystkie moduły i zapisze je w jednym pliku, który można uruchomić w przeglądarce.

Tym poleceniem zainstalujesz Browserify globalnie przez npm:

npm install -g browserify

A tym uruchomisz Browserify na main.js (plik transpilowany do JavaScript) i zapiszesz wszystkie używane moduły pod nazwą bundle.js:

browserify main.js -o bundle.js