TypeScript – część 1

„TypeScript – typowany nadzbiór JavaScriptu, kompilowany do czystego JavaScriptu” – głosi napis na stronie głównej typescriptlang.org. Używam go praktycznie codziennie, w różnych projektach, z różnymi technologiami. Od pewnego czasu, w dużej mierze za sprawą Angulara 2, ale nie tylko, TS zaczął zyskiwać sporą popularność i uznanie w społeczności webdeveloperów.

W tym artykule zakładam, że czytelnicy są zaznajomieni JavaScriptem, a w szczególności z konceptami dodanymi w ECMAScript 2015 takimi jak class oraz letconst.

Czym jest TypeScript

TypeScript jest darmowym i otwartym językiem programowania stworzonym i rozwijanym przez Microsoft od 2012 roku. Jest rozwinięciem JavaScriptu, w którym dodano opcjonalne statyczne typowanie i kilka dodatkowych rzeczy, o których napiszę dalej. TypeScript kompiluje się do JavaScriptu i może być używany zarówno po stronie serwera (node.js), jak i w przeglądarce.

Kompilacja w locie

Są dwa sposoby na korzystanie z TypeScriptu. Pierwszy z nich to użycie skryptu typescript.js, który potrafi w locie kompilować kod TypeScript do JavaScriptu. Jest to przydatna możliwość i korzystam z niej zawsze, gdy wrzucam proste przykłady na strony typu plnkr.co. Niektóre edytory online (jak na przykład CodePen) nie wymagają nawet tego skryptu, a kompilację TypeScript można włączyć w ustawieniach.

TypeScript produkcyjnie

W praktyce jednak do budowania aplikacji znacznie lepiej sprawdza się sposób drugi – czyli skompilowanie TypeScriptu i zapisanie kodu wynikowego jako plik z rozszerzeniem .js, a następnie korzystanie z tego pliku. Jest to rozwiązanie znacznie bardziej wydajne i z tego względu lepsze w przypadku tworzenia czegoś więcej niż proste demo.

TypeScript Playground

Dodatkowo TypeScriptem można pobawić się na tzw. placu zabaw – TypeScript Playground. Po otwarciu tej strony widoczne są dwa pola tekstowe. W lewym wpisujemy kod TS, w prawym zaś widoczne są efekty kompilacji. Dodatkowo w trakcie edycji w URL-u zapisywany jest kod źródłowy, dzięki czemu możemy go skopiować i komuś wysłać, a ta osoba zobaczy dokładnie to samo co my.

Kompatybilność

Wspomniałem, że TS jest nadzbiorem, rozwinięciem JavaScriptu – oznacza to, że dowolny kod napisany w JavaScript jest również prawidłowym kodem w TypeScripcie. Ostatecznie kod napisany w TS kompilowany jest do JS. Co z tego? Są to ogromne zalety z kilku powodów.

Po pierwsze, aby zacząć korzystać z TS nie trzeba od razu poznawać go w całości – nowych aspektów można się uczyć i używać fragmentami, a resztę kodu pisać tak jak zwykły JS.

Ponadto w projekcie, który już jest napisany w JavaScripcie możemy zacząć używać TypeScriptu właściwie w dowolnym momencie. Aktualnie pracuję zresztą nad jednym projektem, który jest w takim etapie przejściowym – duża część plików jest napisana w czystym JavaScripcie, a nowe moduły już w TypeScripcie.

Daje to ogromną elastyczność oraz pozwala na spróbowanie pracy z TS właściwie w dowolnym miejscu. Dodatkowo jest to odpowiedź na jeden z argumentów przeciwko TS: „Co jeśli TypeScript przestanie być rozwijany”? TS daje nam możliwość łatwego powrotu do JavaScriptu, nawet jeśli część aplikacji jest już napisana w TypeScripcie. Nie musimy przepisywać niczego na nowo, wystarczy tylko skompilować TS do JS i dalej pracować na czystym JavaScripcie.

Typy

Statycznie typowanie

Chyba najważniejszą cechą TypeScriptu jest dodanie statycznego, silnego typowania. Statyczne typowanie oznacza, że zmienne mają nadane typy i te typy nie mogą się zmienić1. Na przykład poniższy kod jest całkowicie poprawny w JS:

let x = 1;  
x = 'abc';  
x = new Date();  

Zmienna x nie ma ustalonego typu. Na początku przechowuje liczbę, potem ciąg znaków, a na koniec obiekt z datą. Czy jest to przydatna możliwość? Bez wątpienia daje nam ogromną możliwość ekspresji. Jednak większość doświadczonych programistów na pewno przyzna, że kod napisany w ten sposób jest podatny na błędy i nieczytelny – i muszę przyznać im całkowitą rację. W TypeScripcie zmienne mogą mieć ustalony z góry typ i wtedy niemożliwe jest przypisanie do nich czegoś, co nie jest z tym typem zgodne (na przykład daty do zmiennej z liczbą).

Silne typowanie

Silne typowanie oznacza zaś, że zmienna o ustalonym typie nie może być użyta tam, gdzie oczekiwany jest inny typ2. Mówiąc prościej: Nie możemy porównać liczby z ciągiem znaków, albo przekazać daty do funkcji, która oczekuje liczby. Spójrzmy na przykład kodu w JS:

const x = 1;  
if (x === "1") { /* porównanie liczby ze stringiem */ }

function dodaj(a, b) { return a + b; }  
dodaj(1, 2); // 3  
dodaj("1", "2"); // "12"  

W pierwszym przykładzie porównujemy liczbę ze stringiem. Już na pierwszy rzut oka nie ma to sensu. W drugim przykładzie stworzyliśmy funkcję, która zwraca nieoczekiwane rezultaty, gdy przekażemy parametry o innych typach. Obu tych błędów można uniknąć używając TypeScripta. Co ważniejsze – bez TS te błędy zostaną zauważone dopiero na etapie testowania aplikacji. Natomiast jeśli wykorzystamy TypeScript to informację o pomyłkach dostaniemy już w trakcie kompilacji kodu.

Typy w TypeScript

Spróbujmy więc poprawić kod z poprzednich przykładów tak, aby błędy zakończyły się niepowodzeniem kompilacji. Następnie przejdziemy do bardziej skomplikowanych przykładów.

Typy w TypeScript piszemy po znaku dwukropka:

let x:number;  

Podobnie można też oznaczać argumenty funkcji oraz typ przez nie zwracany:

function round(a:number):string {  
    return a.toFixed(2);
}

Wbudowane podstawowe typy to znane z JavaScriptu:

  • boolean
  • number
  • string
  • array

Dodatkowo TypeScript oferuje również typy bardziej zaawansowane:

  • tuple
  • enum
  • any
  • void

i kilka innych bardziej skomplikowanych konceptów.

Boolean

Jeden z najbardziej podstawowych typów. Reprezentuje wartość logiczną, prawdę lub fałsz: true, false.

Number

Liczby zmiennoprzecinkowe znane z JavaScriptu, włączając w to literały heksadecymalne, oktalne i binarne: 6, 1.2e5 0xbeaf, 0b1010101, 0o765.

String

Ciągi znaków, identyczne do tych w JavaScripcie. Możemy je zapisywać przy pomocy cudzysłowów i apostrofów, wspierane są też template stringi:

const x:string = 'Hello';  
const y:string = "world";

const tpl = `${x}, ${y}!`; // Hello, world!  

Array

Podobnie jak w JS, w TypeScripcie możemy operować na tablicach wartości. Typ tablicowy możemy zapisać na dwa sposoby, a ze względu na to, że przechowują one wartości o określonym typie, podajemy go:

const arr1:Array<number> = [1, 2, 3];  
const arr2:number[] = [1, 2, 3];  

Tuple

Tupla to skończona lista elementów. w TypeScripcie jest to tablica, której długość jest dokładnie znana, a typy wszystkich elementów jasno określone:

const tuple:[number, string] = [1, 'd'];  

Enum

Enumeracja to zbiór nazwanych wartości. Bardzo przydatny dodatek do JavaScriptu, znany z wielu innych języków takich jak C++, Java czy C#. W TS jest to zbiór wartości liczbowych:

enum Suit {  
    Spades,
    Hearts,
    Diamonds,
    Clubs
};

const cards:Suit = Suit.Spades; // 0  

Domyślnie elementy enumeracji są numerowane od zera, ale można to zmienić:

enum Suit {  
    Spades = 123,
    Hearts,
    Diamonds,
    Clubs
};

// Suit.Hearts to 124

Any

Czasem może nam się zdarzyć, że nie będziemy w stanie określić typu jakiejś zmiennej – służy do tego any. Zmienne typu any mogą przyjmować dowolne wartości:

let x:any = 4;  
x = 'a';  
x = new Date();  

Typ any może się okazać przydatny w przypadku tablic przechowujących wartości różnych typów:

const x:Array<any> = [];  
x.push(1);  
x.push('a');  
x.push(new Date);  

Zawsze polecam spróbować zrefaktorować kod tak, aby określenie typu było możliwe. Używanie typu any niweczy wszystkie zalety typowania.

Void

Ten typ oznacza „brak wartości”. Powszechnie używa się go do oznaczania funkcji, które nic nie zwracają:

function showAlert(text:string):void {  
    window.alert(text);
}

Poprawiony kod

Wróćmy więc do oryginalnego kodu. Po dodaniu typów będzie on wyglądał na przykład tak:

const x:number = 1;  
if (x === "1") { /* Błąd kompilacji! */ }

function dodaj(a:number, b:number) { return a + b; }  
dodaj(1, 2); // 3  
dodaj("1", "2"); // Błąd kompilacji!  

Wszystkie błędy polegające na niekonsekwentnych użyciu typów, albo na pomyleniu typów zostają wyłapane już przez kompilator!

Klasy i interfejsy

TypeScript posiada również koncept klas znany z ECMAScript 2015. Pozwala to na myślenie bardziej orientowane-obiektowo i znacznie upraszcza składnię, choć w rzeczywistości pod maską całość opiera się o konstruktory i dziedziczenie prototypowe. Stwórzmy przykładową klasę:

class Animal {  
    name:string;

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

    sayHello():string {  
            return `Hello, my name is ${this.name}!`;
    }
}

const dog = new Animal('Burek');  
dog.sayHello() // 'Hello, my name is Burek!';  

Klasa ta posiada jedno pole typu string o nazwie name oraz jedną metodę zwracającą stringsayHello. Dodatkowo zdefiniowaliśmy również konstruktor, który przyjmuje imię i zapisuje je w polu name. Wewnątrz metody sayHello odwołujemy się do pola name poprzez this.name.

Private, public

Osoby znające inne języki obiektowe na pewno zastanawiają się czy TypeScript pozwala na tworzenie pól i metod prywatnych. Otóż tak, pozwala! Domyślnie wszystkie elementy klasy są publiczne, jednak można to zmienić poprzedzając ich deklarację słowem kluczowym private. Podobnie, można również explicite dodać słowo kluczowe public. Pole name oznaczyłem jako prywatne, bo nie chcę, aby dostęp do tego pola był możliwy z zewnątrz, natomiast metoda sayHello ma być dostępna dla wszystkich:

class Animal {  
    private name:string;

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

    public sayHello():string {  
            return `Hello, my name is ${this.name}!`;
    }
}

Dobrą praktyką jest oznaczanie jako private wszystkiego co tylko się da, tak aby z zewnątrz był dostęp wyłącznie do tych pól i metod, które są potrzebne. Nazywa się to enkapsulacją lub hermetyzacją.

Interfejsy

Deklaracja klasy z użyciem słowa kluczowego class tworzy w TypeScripcie tak naprawdę dwie rzeczy:

  • typ reprezentujący instancje
  • funkcję konstruktora

Czasem jednak ten konstruktor nie jest nam potrzebny i jedyne czego chcemy to zdefiniować kształt obiektu. Innymi słowy, używamy obiektów o określonej strukturze i chcemy to jakoś opisać. Przykładowo chcemy opisać obiekt reprezentujący wiadomość. Wiadomość ma treść, nadawcę i odbiorcę, i w JavaScripcie wygląda tak:

const message = {  
    text: 'Hello',
    sender: 'Michal',
    receiver: 'Anna'
};

Możemy sformalizować kształt tego obiektu tworząc interfejs:

interface Message {  
    text:string;
    sender:string;
    receiver:string;
}
const message:Message = {  
    text: 'Hello',
    sender: 'Michal',
    receiver: 'Anna'
};

Dzięki temu jeśli na przykład zrobimy literówkę lub przypiszemy do message przez pomyłkę inną zmienną – dostaniemy błąd:

const message:Message = {  
    text: 'Hello',
    sender: 'Michal',
    recevier: 'Anna'    // Błąd kompilacji! Literówka!
};

Na potrzeby tego wpisu interfejsy możemy traktować jako wymagania stawiane obiektom, jednak ich możliwości są znacznie większe i opiszę to w kolejne części kursu TypeScript.

Definicje typów

Wszystko jest pięknie, gdy w aplikacje znajduje się wyłącznie kod napisany w TypeScripcie. Co jednak, gdy chcemy z poziomu TypeScript skorzystać z istniejących bibliotek, które napisane w TS nie są? Brakuje przecież informacji o typach.

Pliki .d.ts

Odpowiedzią na ten problem są pliki z rozszerzeniem .d.ts – są to tzw. pliki definicji i zawierają wyłącznie informacje o typach, bez implementacji. Pliki te możemy pobrać przy pomocy narzędzia o nazwie typings. Przykładowo chcemy mieć informację o typach dla biblioteki bluebird, wydajemy więc polecenie:

typings install bluebird  

Pliki automatycznie są pobieranie, a niewielka konfiguracja naszego środowiska pozwoli nam z nich korzystać. Więcej na ten temat można doczytać w dokumentacji typings/typings.

Podsumowanie

W tym wprowadzeniu do TypeScript poznaliśmy podstawy tego języka. Dowiedzieliśmy się jakie podstawowe typy są dostępne i w jaki sposób ich używać. Ponadto nauczyliśmy się tworzyć klasy z publicznymi i prywatnymi polami i metodami oraz poznaliśmy podstawy interfejsów.

W kolejnej części nauczymy się używać bardziej zaawansowanych klas, klas abstrakcyjnych oraz dziedziczenia. Do tego będziemy też implementować interfejsy w klasach i skorzystamy z bardziej zaawansowanych typów, a także dowiemy się co to jest inferencja typów i dlaczego jest taka fajna 🙂 Zachęcam do komentowania!

  • Kuba

    Dobrze, że powstał ten wpis, właściwie to powinien on powstać dużo wcześniej, niemniej lepiej późno niż wcale 🙂 zaciekawiły mnie te „większe możliwości interfejsów”, także czekam na kolejny wpis o TS 🙂

  • Czy TS jest użyteczny wyłącznie w kontekście aplikacji Angularowych? Czy przydaje się również w prostych skryptach wykorzystywanych w stronach WWW do manipulacji DOM’em?

    • Tak, jak napisał Michał – TypeScript to tylko nadzbiór JavaScriptu. Można go stosować praktycznie z każdym frameworkiem. Kwestia osobistych preferencji. 😉

    • Dokładnie tak. TypeScript nie ma związku z AngularJS. Możesz z niego korzystać gdzie tylko chcesz, kiedy tylko chcesz 😉 A to czy się przydaje musisz ocenić sam. Moim zdaniem: Tak.

  • Franek

    Świetny artykuł, właśnie spędzam mega dużo czasu nad ogarnięciem Angulara 2. Do tej pory siedziałem na wersji 1 i nie wierzyłem gdy mówili że wersja 2 to zupełnie co innego.
    Czy ktoś zna dobra stronę gdzie wyjaśnione są wszelkie pojecia, zasady, classy itp w TP?

  • Bardzo przystępnie napisane. Dziękuję 🙂

    • Dzięki 😉 Pod jakim kątem chciałbyś zobaczyć kolejne wpisy? 🤔

      • „chciałAbyś” 😉 Z serii o TS mam u Ciebie jeszcze dwa do przeczytania. 🙂 Dopiero zaczynam w TS się rozglądać, więc trudno mi na tym etapie zgłosić jakieś zapotrzebowanie.

  • Pingback: Wstęp do Angular 2 – Type of Web()

  • Pingback: Kurs TypeScript – część 2 • Type of Web()