TypeScript – Co to jest? TypeScript, a JavaScript?
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ć?
Co to jest 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<number> = [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;
TypeScript – 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!'});
TypeScript – 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.
TypeScript – 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) w TypeScript
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<T> {
print ( message : T) {
if (message['type']) {
console.log( message['type'] + ': ' + message['text'] );
} else {
console.log( message['text'] );
}
}
}
let alert1 = new Alert<Message>();
let alert2 = new Alert<SimpleMessage>();
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