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 let
i const
.
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ą string
– sayHello
. 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!