Sashy i Michaelowi,

którzy być może pewnego dnia

również zapałają miłością do typów

Dane oryginału

Authorized Polish translation of the English edition of Programming TypeScript, ISBN 9781492037651

© 2019 Boris Cherny

This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same.

Przekład Natalia Chounlamany-Turalska na zlecenie WITKOM Witold Sikorski

Projekt okładki polskiego wydania INT-MEDIA Dawid Mazur na podstawie oryginału

Wydawca Edyta Kawala

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Grażyna Polkowska-Nowak

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN

Aneta Pudzisz/Woblink

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2020

ISBN 978-83-01-21122-6

eBook został przygotowany na podstawie wydania papierowego z 2020 r., (wyd. I)

Warszawa 2020

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl, reklama@pwn.pl

www.pwn.pl

Przedmowa

To książka dla programistów w szerokim tego słowa znaczeniu: profesjonalnych inżynierów JavaScriptu, specjalistów C#, zwolenników Javy, miłośników Pythona, pasjonatów Ruby, fanatyków Haskella. Niezależnie od tego, w jakim języku programujesz, o ile masz pewne doświadczenie i znasz podstawy funkcji, zmiennych, klas i błędów, to książka dla ciebie. Pewne doświadczenie w pracy z JavaScriptem, m.in. podstawowa znajomość modelu DOM i sieci, będzie stanowiło nieocenioną pomoc. Chociaż nie zagłębiamy się w szczegóły, te domeny stanowią źródło doskonałych przykładów, które będą mniej zrozumiałe dla niewtajemniczonych.

Niezależnie od praktykowanego języka wszyscy programiści wiedzą, jak to jest śledzić wyjątki, analizując poszczególne wiersze kodu w poszukiwaniu błędu i sposobu jego naprawienia. Takich właśnie doświadczeń ma nam oszczędzić TypeScript, który automatycznie bada kod i wskazuje przeoczone błędy.

Nic nie szkodzi, jeśli nie miałeś okazji programować w języku z typowaniem statycznym. Wyjaśnię, czym są typy, jak efektywnie wykorzystywać je do poprawiania stabilności programów, lepszego dokumentowania kodu i skalowania aplikacji na większą liczbę użytkowników, inżynierów i serwerów. Gdy to możliwe, będę unikał wielkich słów, wyjaśniając koncepcje w intuicyjny, łatwy do zapamiętania i praktyczny sposób z wykorzystaniem wielu przykładów, które pomagają w skonkretyzowaniu problemów.

To jest właśnie najlepsze w TypeScripcie: w odróżnieniu od wielu innych języków z systemami typów TypeScriptu jest praktyczny. Wprowadza zupełnie nowe koncepcje, które umożliwiają zwięźlejszą i precyzyjniejszą komunikację i tym samym pisanie aplikacji w sposób, który jest ciekawy, nowoczesny i bezpieczny.

Układ książki

Ta książka ma dwa cele: pomóc czytelnikom zrozumieć działanie języka TypeScript (teoria) i dostarczyć wielu przydatnych porad dotyczących pisania kodu produkcyjnego TypeScriptu (praktyka).

Ponieważ TypeScript jest tak praktycznym językiem, teoria szybko staje się praktyką i zdecydowana część książki łączy oba aspekty, przy czym kilka pierwszych rozdziałów koncentruje się na teorii, a kilka ostatnich – na praktyce.

Zacznę od wytłumaczenia, czym są kompilatory, funkcje sprawdzania typów oraz typy. Później ogólnie omówię różne typy i operatory typów w TypeScripcie, ich zastosowania i sposoby użycia. Po wprowadzeniu podstawowych zagadnień zajmę się bardziej zaawansowanymi tematami, takimi jak wyrafinowane funkcje systemu typów, obsługa błędów i programowanie asynchroniczne. Na zakończenie pokażę, jak używać TypeScriptu w połączeniu z twoim ulubionym frameworkiem (front-end i back-end), migrując istniejący projekt JavaScriptu do TypeScriptu i uruchamiając aplikację w środowisku produkcyjnym.

Na końcu większości rozdziałów umieściłem zestaw ćwiczeń. Warto spróbować je wykonać, aby lepiej zrozumieć omawiane zagadnienie dzięki zastosowaniu go w praktyce niż tylko w wyniku przeczytania rozdziału. Rozwiązania ćwiczeń są dostępne w internecie pod adresem: https://github.com/bcherny/programming-typescript-answers.

Styl

W całej książce starałem się stosować ten sam styl programowania. Niektóre aspekty tego stylu wynikają z osobistych preferencji, np.

Używam średników tylko wtedy, gdy to konieczne.

Stosuję wcięcia o szerokości dwóch spacji.

Używam krótkich nazw zmiennych, takich jak: a, f czy _ , gdy program to szybki przykład lub gdy struktura programu jest ważniejsza niż jego szczegóły.

Ten styl kodowania ilustruje również pewne zalecenia, których moim zdaniem warto przestrzegać. Oto kilka z nich:

Należy stosować najnowszą składnię i funkcje JavaScriptu (najnowsza wersja JavaScript jest zwykle nazywana po prostu „esnext”). Dzięki temu kod jest zgodny z najnowszymi standardami, co poprawia jego interoperacyjność i wyszukiwalność oraz może pomóc w szybszym wdrażaniu nowych pracowników. Ponadto zapewnia dostęp do wielu wspaniałych, nowoczesnych funkcji JavaScriptu, takich jak funkcje strzałkowe, obietnice i generatory.

W większości sytuacji należy zachowywać niezmienność struktur danych, stosując operatory rozwijania (...)1.

Wszystko powinno mieć typ, najlepiej wnioskowany. Nie należy nadużywać jawnych typów. Takie podejście pomaga w uzyskiwaniu jasnego, lakonicznego kodu i zwiększa bezpieczeństwo dzięki ujawnianiu nieprawidłowych typów zamiast ich mitygowania.

Kod powinien być uogólniony i służyć do wielokrotnego użytku, dlatego warto zaprzyjaźnić się z polimorfizmem (patrz „Polimorfizm” na stronie 64).

Oczywiście, nie są to nowe pomysły. TypeScript działa jednak szczególnie dobrze, gdy są one stosowane. Wbudowany kompilator TypeScriptu wspierający starsze wersje, obsługa typów tylko do odczytu, zaawansowane wnioskowanie typów, głębokie wsparcie dla polimorfizmu i w pełni strukturalny system typów sprzyjają programowaniu w dobrym stylu, a język pozostaje bardzo ekspresyjny i wierny bazowemu językowi JavaScript.

Kilka uwag, zanim zaczniemy pracę.

JavaScript nie odsłania wskaźników i referencji, zamiast tego ma typy wartościowe i typy referencyjne. Wartości są niezmienne i zaliczają się do nich łańcuchy, liczby oraz wartości logiczne, natomiast referencje wskazują zazwyczaj modyfikowalne struktury danych, takie jak: tablice, obiekty, funkcje. W tej książce termin „wartość” jest często stosowany swobodnie w odniesieniu zarówno do wartości, jak i referencji JavaScriptu.

Życie czasami zmusza nas do pisania niedoskonałego kodu TypeScriptu, gdy musimy zapewnić interoperacyjność z kodem JavaScriptu lub gdy korzystamy ze starego kodu bądź z zewnętrznych bibliotek z nieprawidłowymi typami, lub gdy po prostu nam się spieszy. Ta książka uczy, jak należy pisać kod TypeScript, i przekonuje, dlaczego warto walczyć z pokusą obniżania jakości. W praktyce jednak kryteria oceniania poprawności kodu zależą od ciebie i twojego zespołu.

Konwencje zastosowane w książce

W tej książce zostały zastosowane następujące konwencje typograficzne:

Kursywa

Wskazuje nowe terminy, adresy URL oraz adresy e-mail, nazwy i rozszerzenia plików.

Stała szerokość

Stosowana w listingach programów, jak również wewnątrz akapitów do odwoływania się do elementów programu, takich jak: nazwy zmiennych lub funkcji, typy danych, zmienne środowiskowe, instrukcje, słowa kluczowe.

Stała szerokość i kursywa

Wskazuje tekst, który należy zastąpić wartościami podanymi przez użytkownika lub wynikającymi z kontekstu.

Ten element sygnalizuje wskazówkę lub sugestię.

Ten element sygnalizuje uwagę ogólną.

Ten element sygnalizuje ostrzeżenie lub przestrogę.

Korzystanie z przykładów kodu

Materiały pomocnicze (przykłady kodu, ćwiczenia itd.) można pobrać ze strony https://github.com/bcherny/programming-typescript-answers.

Ta książka ma pomóc w realizowaniu zadań. Zasadniczo prezentowane w niej przykłady kodu można wykorzystywać we własnych programach i we własnej dokumentacji. Nie trzeba kontaktować się z nami w celu uzyskania zezwolenia, o ile nie kopiuje się znacznej części kodu. Na przykład pisanie programu wykorzystującego różne fragmenty kodu z książki nie wymaga zezwolenia. Sprzedaż i dystrybucja płyty CD-ROM z przykładami z książek O’Reilly Media wymaga zezwolenia. Odpowiadanie na pytanie poprzez zacytowanie tej książki i umieszczonego w niej przykładu kodu nie wymaga zezwolenia. Umieszczanie znaczącej części przykładowego kodu z tej książki w dokumentacji własnego produktu wymaga zezwolenia.

Będziemy wdzięczni za podanie źródła, choć nie wymagamy tego. Odwołanie do źródła zazwyczaj zawiera tytuł, autora, wydawnictwo i numer ISBN. Na przykład: Programowanie w języku TypeScript, Boris Cherny (O’Reilly). Copyright 2019 Boris Cherny, 978-1-492-03765-1.

W razie wątpliwości, czy użycie przykładów kodu wykracza poza ogólnie przyjęte zasady lub określone wyżej zezwolenia, prosimy o kontakt pod adresem: permissions@oreilly.com.

O’Reilly Online Learning

O’Reilly Media od niemal 40 lat oferuje szkolenia techniczne i biznesowe oraz dostarcza wiedzę, która pomaga firmom w odnoszeniu sukcesu.

Oferujemy jedyną w swoim rodzaju sieć ekspertów i innowatorów, którzy dzielą się swoją wiedzą i doświadczeniem za pośrednictwem książek, artykułów, konferencji i naszej platformy e-learningowej. Platforma e-learningowa O’Reilly zapewnia kolekcję dostępnych na żądanie warsztatów na żywo, kompleksowych ścieżek szkoleniowych, interakcyjnych środowisk programistycznych, a także ogromny zbiór tekstów i wideo firmy O’Reilly oraz ponad dwustu innych wydawców. Dodatkowe informacje można znaleźć na stronie http://oreilly.com/.

Jak się z nami skontaktować

Komentarze i pytania dotyczące tej książki prosimy kierować do wydawnictwa:

O’Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (w USA lub Kanadzie)

707-829-0515 (połączenia międzynarodowe lub lokalne)

707-829-0104 (faks)

Przygotowaliśmy stronę internetową dotyczącą tej książki, na której umieszczamy erratę, przykłady i dodatkowe informacje. Jest ona dostępna pod adresem: https://oreilly/programming-typescript.

Aby przekazać swój komentarz lub zadać pytanie techniczne dotyczące tej książki, można wysłać e-mail na adres: bookquestions@oreilly.com.

Dodatkowe informacje dotyczące naszych książek, kursów, konferencji i aktualnych wydarzeń zawiera strona internetowa: http://www.oreilly.com.

Można nas znaleźć na Facebooku: http://facebook.com/oreilly.

Można nas śledzić na Twitterze: http://twitter.com/oreillymedia.

Można także oglądać nas na kanale YouTube: http://www.youtube.com/oreillymedia.

Podziękowania

Na pisanie tej książki przeznaczyliśmy rok wczesnych poranków, pracowitych nocy i weekendów. Tak naprawdę stanowi ona jednak efekt wielu lat pisania kodu i gryzmolenia.

Dziękuję wydawnictwu O’Reilly za danie mi szansy napisania tej książki oraz mojej redaktorce Angeli Rufino za wsparcie w tym procesie. Dziękuję Nickowi Nance za podrozdział „API zapewniające bezpieczeństwo typów” na stronie 204 oraz Shyamowi Seshadri za podrozdział „Angular” na stronie 201. Dziękuję redaktorom technicznym: Danielowi Rosenwasserowi z zespołu TypeScriptu, który poświęcił nierozsądną ilość czasu na czytanie tego manuskryptu i oprowadzanie mnie po niuansach systemu typów TypeScriptu, a także Jonathanowi Creamerowi, Yakovowi Fainowi, Paulowi Buyingowi oraz Rachel Head za uwagi techniczne i opinie. Dziękuję mojej rodzinie: Lizie i Ilyi, Vadimowi, Rozie i Alikowi, Fainie i Yosifowi za zachęcanie mnie do realizacji tego projektu.

Przede wszystkim jednak dziękuję mojej partnerce Sarze Gilford, która wspierała mnie w procesie pisania, nawet gdy oznaczało to anulowanie planów na weekend, zarywanie nocy w celu pisania i programowania oraz o wiele za dużo spontanicznych rozmów o szczegółach systemu typów. Bez Ciebie nie mógłbym tego dokonać i będę zawsze Ci wdzięczny za wsparcie.

ROZDZIAŁ 1

Wprowadzenie

Czyli zdecydowałeś się kupić książkę o TypeScripcie. Dlaczego?

Być może masz już dość dziwnych błędów JavaScriptu typu cannot read property coś tam coś tam undefined. A może słyszałeś, że TypeScript podnosi skalowalność kodu i chciałeś sprawdzić, o co tyle szumu? Może jesteś programistą C# i zastanawiasz się, czy nie spróbować całego tego JavaScriptu. A może jesteś programistą języka funkcyjnego i zdecydowałeś, że nadszedł czas wrzucić wyższy bieg. Może twój szef ma już tak dość problemów produkcyjnych powodowanych przez twój kod, że dał ci tę książkę pod choinkę (powiedz stop, gdy będziemy na dobrym tropie).

Niezależnie od osobistych przyczyn, to prawda, co mówią o języku TypeScript – naprawdę znajdzie on zastosowanie w nowej generacji aplikacji sieci Web, aplikacji mobilnych i Internetu rzeczy (Internet of Things – IoT). Zwiększy on bezpieczeństwo programów, będzie pełnił funkcję dokumentacji dla nas i przyszłych programistów, ułatwi przeprowadzanie refaktoryzacji i sprawi, że połowa naszych testów jednostkowych stanie się niepotrzebna („Jakie testy jednostkowe?”). Dzięki TypeScript podwoi się twoja produktywność w pracy, a przy odrobinie szczęścia uda ci się wyciągnąć szczęśliwy los na loterii w prima aprilis.

Ale najpierw pozwól nam wyjaśnić, co mamy na myśli, mówiąc o „bezpieczeństwie”. Oczywiście chodzi nam o bezpieczeństwo typów.

Bezpieczeństwo typów

Wykorzystywanie typów do powstrzymywania programów przed podejmowaniem nieprawidłowych akcji1.

Oto kilka przykładów nieprawidłowych akcji:

mnożenie liczby i listy,

wywoływanie funkcji na liście łańcuchów, mimo że tak naprawdę potrzebuje ona listy obiektów,

wywoływanie metody na obiekcie, który w rzeczywistości nie ma tej metody,

importowanie modułu, który został usunięty.

Niektóre języki programowania próbują, jak najlepiej potrafią, radzić sobie z tego typu błędami. Próbują domyślić się, do czego miała prowadzić nieprawidłowa akcja, ponieważ każdy robi, co może, prawda? Weźmy pod uwagę następujący kod JavaScriptu:

3 + [] // Otrzymujemy łańcuch “3”

let obj={}

obj.foo // Otrzymujemy undefined

function a(b) {

return b/2

}

a(“z”) // Otrzymujemy NaN

Jak widać, zamiast zgłaszać wyjątek w odpowiedzi na próbę wykonania nieprawidłowej akcji, JavaScript próbuje zaradzić sytuacji i unikać wyjątków, gdy jest to możliwe. Czy JavaScript chce pomóc? Na pewno. Czy ułatwia szybkie zidentyfikowanie błędów? Raczej nie.

A teraz wyobraźmy sobie, że JavaScript zgłasza więcej wyjątków, zamiast po cichu radzić sobie ze stworzoną przez nas sytuacją. Moglibyśmy otrzymać następujące informacje zwrotne:

3 + [] // Błąd: Czy naprawdę chcesz dodać liczbę i tablicę?

let obj={}

obj.foo // Błąd: Zapomniałeś zdefiniować w obj właściwość “foo”.

function a(b) {

return b/2

}

a(“z”) // Błąd: Funkcja “a” oczekuje liczby,

// a przekazałeś łańcuch.

Proszę nas źle nie zrozumieć: gotowość do rozwiązywania naszych błędów to kapitalna cecha języka programowania (gdyby było to możliwe nie tylko w przypadku programów). W JavaScript ta funkcja oddala jednak moment popełnienia błędu w kodzie od momentu dowiadywania się o tym, że popełniliśmy błąd w kodzie. A to często oznacza, że o naszym błędzie dowiadujemy się dopiero od innych osób.

W związku z tym pojawia się pytanie, kiedy dokładnie JavaScript informuje nas, że popełniliśmy błąd.

Odpowiedź brzmi: po uruchomieniu programu. Program może zostać uruchomiony, gdy testujemy go w przeglądarce lub gdy użytkownik odwiedzi naszą stronę, lub gdy wykonamy test jednostkowy. Zdyscyplinowani programiści, którzy piszą dużo testów jednostkowych i e2e (end-to-end), wykonują testy dymne przed wypchnięciem kodu i testują go lokalnie przed przekazaniem do użytkowników, zazwyczaj dowiadują się o swoich błędach przed użytkownikami. Ale co z resztą programistów?

Tu z pomocą przychodzi TypeScript. Jego dużą zaletą jest nie tylko dostarczanie pomocnych komunikatów o błędach, lecz także moment ich przekazywania: TypeScript pokazuje komunikaty o błędach w edytorze tekstowym w czasie wpisywania kodu. To oznacza, że nie musimy polegać na tym, że tego typu błędy zostaną wykryte przez testy jednostkowe, testy dymne lub naszych współpracowników: TypeScript wykrywa te błędy i informuje nas o nich podczas pisania programu. Zobaczmy, co TypeScript mówi o naszym poprzednim przykładzie:

3 + [] // Błąd TS2365: Nie można zastosować operatora ‘+’ do typów ‘3’

// i ‘never[]’.

let obj = {}

obj.foo // Błąd TS2339: Właściwość ‘foo’ nie istnieje w typie ‘{}’.

function a(b: number) {

return b/2

}

a(“z”) // Błąd TS2345: Nie można przypisać argumentu typu ‘”z”’

// do parametru typu’number’.

Nie tylko eliminuje on całe klasy błędów związanych z typami, ale tak naprawdę zmienia sposób pisania kodu. W efekcie zaczynamy od naszkicowania programu na poziomie typów, a dopiero później wypełniamy go na poziomie wartości2 – przypadki brzegowe bierzemy pod uwagę już w fazie projektowania i tworzymy programy, które są prostsze, szybsze, bardziej zrozumiałe i łatwiejsze w utrzymywaniu.

Gotowy do rozpoczęcia przygody? W takim razie wyruszamy!

ROZDZIAŁ 2

TypeScript: obraz ogólny

W kilku kolejnych rozdziałach przedstawimy język TypeScript, wyjaśnimy, w jaki sposób działa kompilator TypeScriptu (w skrócie TSC – TypeScript Compiler) i przedstawimy przegląd funkcji TypeScriptu oraz wzorców, które można przy ich użyciu implementować. Zaczniemy od kompilatora.

Kompilator

W zależności od tego, w jakim języku wcześniej programowałeś (oczywiście zanim zdecydowałeś się kupić tę książkę i już na zawsze pozostać przy bezpieczeństwie typów), możesz w różny sposób postrzegać działanie programów. TypeScript pracuje inaczej niż większość popularnych języków, takich jak JavaScript czy Java, dlatego musimy upewnić się, że dobrze się rozumiemy.

Zacznijmy od podstaw: programy to pliki, które zawierają pewien tekst napisany przez programistę. Ten tekst jest parsowany przez specjalny program nazywany kompilatorem, który przekształca go w drzewo składniowe (abstract syntax tree, AST), czyli strukturę danych, która ignoruje takie elementy, jak: odstępy, komentarze i twoje stanowisko w debacie tabulatory kontra spacje. Następnie kompilator konwertuje to AST do niskopoziomowej reprezentacji nazywanej kodem bajtowym. Ten kod bajtowy można przekazać do innego programu nazywanego środowiskiem uruchomieniowym w celu jego ewaluacji i otrzymania wyniku. A zatem gdy uruchamiamy program, tak naprawdę instruujemy środowisko uruchomieniowe, aby przeprowadziło ewaluację kodu bajtowego wygenerowanego przez kompilator na podstawie AST sparsowanego na bazie naszego kodu źródłowego. Szczegóły mogą się różnić, ale ogólny schemat dotyczy większości języków.

Podsumowując, oto podstawowe kroki:

Program jest parsowany do AST.

AST jest kompilowane do kodu bajtowego.

Środowisko uruchomieniowe przeprowadza ewaluację kodu bajtowego.

TypeScript różni się tym, że zamiast dokonywać kompilacji bezpośrednio do kodu bajtowego, przeprowadza kompilację do... kodu JavaScriptu! Następnie ten kod JavaScriptu można, jak zwykle, wykonać w przeglądarce, przy użyciu NodeJS lub własnoręcznie za pomocą kartki i długopisu (jeśli czytasz to po wybuchu buntu maszyn).

Być może zastanawiasz się: „Moment! W poprzednim rozdziale napisano, że TypeScript zwiększa bezpieczeństwo kodu! Kiedy to zachodzi?”.

Dobre pytanie. Faktycznie pominęliśmy zasadniczy krok: po tym, jak kompilator TypeScriptu generuje AST programu, ale zanim emituje kod, wykonuje na kodzie sprawdzanie typów.

Program do sprawdzania typów

Specjalny program, który sprawdza bezpieczeństwo typów w kodzie.

W tym sprawdzaniu typów kryje się magia języka TypeScript. W ten sposób TypeScript upewnia się, że program działa zgodnie z oczekiwaniami, nie zawiera oczywistych błędów i na pewno twój szczęśliwy los w primaaprilisowej loterii czeka na ciebie.

Gdy uwzględnimy sprawdzanie typów i generowanie kodu JavaScriptu, proces kompilowania kodu TypeScriptu wygląda mniej więcej tak, jak na rysunku 2.1:

Rysunek 2.1. Kompilowanie i wykonywanie kodu TypeScriptu

Kroki 1–3 są wykonywane przez TSC, natomiast kroki 4–6 przez środowisko uruchomieniowe JavaScriptu, które działa w przeglądarce, NodeJS lub innym wykorzystywanym silniku JavaScriptu.

Kompilatory i środowiska uruchomieniowe JavaScriptu są często umieszczane w jednym programie nazywanym silnikiem. Zazwyczaj to właśnie z nim podejmują interakcję programiści. W ten sposób działa V8 (silnik NodeJS, Chrome i Opera), SpiderMonkey (Firefox), JSCore (Safari) oraz Chakra (Edge). Dzięki takiemu rozwiązaniu JavaScript sprawia wrażenie, jakby był językiem interpretowanym.

W tym procesie typy zdefiniowane w programie są wykorzystywane w krokach 1–2, ale nie w kroku 3. Warto ponownie podkreślić, że TSC, kompilując nasz kod z języka TypeScript do języka JavaScript, nie będzie uwzględniał typów. To oznacza, że typy zdefiniowane w programie nie będą wpływały na generowany wynik programu i będą służyły jedynie do sprawdzania typów. Ta cecha sprawia, że można swobodnie wypróbowywać, modyfikować i udoskonalać typy w programie bez obaw, że uszkodzi to działanie aplikacji.

System typów

Nowoczesne języki programowania mają różne rodzaje systemów typów.

System typów

Zbiór reguł, które program do sprawdzania typów wykorzystuje do przypisywania typów w programie.

Zasadniczo wyróżniamy dwa rodzaje systemów typów: takie, w których kompilator wymaga określenia typu wszystkich obiektów przy użyciu jawnej składni, oraz takie, które potrafią automatycznie wywnioskować typy obiektów. Oba podejścia mają pewne wady i zalety1.

TypeScript czerpie inspiracje z obu systemów typów – możemy dodawać jawne adnotacje typów lub pozwolić TypeScriptowi automatycznie wywnioskować większość typów.

Do jawnego pokazywania kompilatorowi TypeScriptu, jakich typów chcemy użyć, służą adnotacje. Adnotacje mają postać wartość: typ i mówią programowi do sprawdzania typów: „Hej! Widzisz tę wartość? Jej typ to typ”. Przyjrzyjmy się kilku przykładom (w komentarzu na końcu każdego wiersza określiliśmy faktyczny typ zastosowany przez kompilator TypeScriptu):

let a: number = 1 // a jest liczbą

let b: string = ‘hello’ // b jest łańcuchem

let c: boolean[] = [true, false] // c jest tablicą wart. logicznych

A gdy chcemy, aby TypeScript sam wywnioskował typy, wystarczy pominąć je i pozostawić to zadanie kompilatorowi:

let a = 1 // a jest liczbą

let b = ‘hello’ // b jest łańcuchem

let c = [true, false] // c jest tablicą wart. logicznych

Od razu można zauważyć, jak dobrze TypeScript radzi sobie z automatycznym wnioskowaniem typów. Gdy pominęliśmy adnotacje, typy nie zmieniły się! W tej książce będziemy stosować adnotacje tylko wtedy, gdy to konieczne – w pozostałych sytuacjach pozwolimy, aby TypeScript magicznie je wywnioskował.

Zasadniczo dobrze jest pozostawić TypeScriptowi automatyczne wnioskowanie większości typów, minimalizując ilość kodu z jawnie zdefiniowanymi typami.

JavaScript versus TypeScript

Teraz przyjrzyjmy się systemowi typów w TypeScripcie i porównajmy go z systemem typów w JavaScripcie. Tabela 2.1 przedstawia zestawienie. Zrozumienie różnic daje wyobrażenie, w jaki sposób działa TypeScript.

Tabela 2.1. Porównanie systemów typów w JavaScripcie i TypeScripcie

Cecha systemu typów

JavaScript

TypeScript

Jak wiązane są typy?

dynamicznie

statycznie

Czy typy są automatycznie przekształcane?

tak

nie (z reguły)

Kiedy sprawdzane są typy?

w czasie wykonania

w czasie kompilacji

Kiedy ujawniają się błędy?

w czasie wykonania (z reguły)

w czasie kompilacji (z reguły)

Jak wiązane są typy?

Dynamiczne wiązanie typów oznacza, że JavaScript musi tak naprawdę uruchomić nasz program, aby poznać zastosowane w nim typy. JavaScript nie zna typów przed uruchomieniem programu.

TypeScript jest językiem stopniowo typowanym. To oznacza, że TypeScript działa najlepiej, gdy w czasie kompilacji zna typy wszystkich obiektów w programie, ale może skompilować program nawet wtedy, gdy nie zna wszystkich typów. W programie, w którym typy nie zostały zdefiniowane, TypeScript potrafi automatycznie wywnioskować pewne typy i wychwycić niektóre błędy, jednak gdy nie zna wszystkich typów, może przeoczyć wiele błędów, które trafiają do użytkowników.

Stopniowe typowanie bardzo pomaga w migracji istniejącej bazy kodu JavaScriptu bez typów do TypeScriptu z typami (dodatkowe informacje można znaleźć w części „Stopniowa migracja z JavaScriptu do TypeScriptu” na stronie 230), ale w innych sytuacjach należy dążyć do uzyskania 100-procentowego pokrycia dla typów. Takie podejście zostało zastosowane również w tej książce, o ile nie zaznaczyliśmy inaczej.

Czy typy są automatycznie przekształcane?

JavaScript jest językiem o słabym typowaniu. To oznacza, że jeśli wykonamy nieprawidłowe działanie, np. dodamy liczbę do tablicy (jak w rozdziale 1), JavaScript zastosuje wiele reguł w celu odgadnięcia naszych intencji i spróbuje zrobić co może w danej sytuacji. Przeanalizujmy pewien przykład ilustrujący, w jaki sposób JavaScript wyznacza wynik działania 3 + [1]:

JavaScript zauważa, że 3 jest liczbą, a [1] jest tablicą.

Ponieważ używamy operatora +, zakłada, że chcemy połączyć je ze sobą.

W niejawny sposób przekształca 3 w łańcuch, co daje “3”.

W niejawny sposób przekształca [1] w łańcuch, co daje “1”.

Łączy wyniki, co daje “31”.

Moglibyśmy osiągnąć ten sam efekt również w jawny sposób (eliminując kroki 1, 3 i 4):

3 + [1]; // daje w wyniku “31”

(3).toString() + [1].toString() // daje w wyniku “31”

Podczas gdy JavaScript próbuje pomóc, automatycznie wykonując sprytne przekształcenia typów, TypeScript skarży się, gdy podejmujemy nieprawidłowe działanie. Gdy uruchomimy ten sam kod JavaScriptu za pomocą TSC, otrzymamy błąd informujący, że nie można stosować operatora ‘+’ na tych typach:

3 + [1]; // Błąd TS2365: Nie można zastosować operatora

// ‘+’ do typów ‘3’ i ‘number[]’.

(3).toString() + [1].toString() // daje w wyniku “31”

Jeśli podejmiemy działanie, które wydaje się nieprawidłowe, TypeScript zgłasza zażalenie i wycofuje je dopiero wtedy, gdy w jawny sposób wyrazimy nasze intencje. Takie zachowanie ma sens: komu przyszłoby do głowy dodawanie liczby do tablicy w celu otrzymania łańcucha (oczywiście poza Bavmordą, czarownicą JavaScriptu, która siedzi w piwnicy pewnego start-upa i programuje przy blasku świec)?

Tego typu niejawne przekształcania, jakie przeprowadza JavaScript, mogą być źródłem trudnych do zlokalizowania błędów i stanowią zmorę wielu programistów JavaScriptu. To utrudnia pojedynczym inżynierom wykonywanie zadań i dodatkowo komplikuje skalowanie kodu w dużych zespołach, ponieważ każdy członek zespołu musi zrozumieć niejawne założenia przyjęte w kodzie.

W skrócie, gdy przekształcanie typów jest konieczne, należy przeprowadzać je w sposób jawny.

Kiedy sprawdzane są typy?

W większości sytuacji JavaScript nie zwraca uwagi na przekazywane typy i próbuje jak najlepiej dostosować to, co otrzymał, do tego, czego oczekiwał.

Natomiast TypeScript sprawdza kod pod kątem typów w czasie kompilacji (drugi krok na przedstawionej na początku rozdziału liście), dlatego nie trzeba uruchamiać kodu, aby zobaczyć przedstawiony w poprzednim przykładzie błąd. TypeScript statycznie przeanalizuje nasz kod pod kątem tego rodzaju błędów i pokaże je przed uruchomieniem. Jeśli kod nie kompiluje się, często oznacza to, że popełniliśmy błąd i musimy naprawić go, zanim spróbujemy uruchomić kod.

Rysunek 2.2 ilustruje, co się stało po wpisaniu kodu ostatniego przykładu w VSCode (naszym ulubionym edytorze).

Rysunek 2.2. Błąd TypeError zgłoszony przez VSCode

Gdy wybrany edytor kodu ma dobre rozszerzenie TypeScriptu, błąd jest wskazywany już w czasie wpisywania za pomocą czerwonej falistej linii podkreślającej kod. To znacznie skraca czas między napisaniem kodu a dowiadywaniem się o błędzie i poprawianiem kodu w celu wyeliminowania tego błędu.

Kiedy ujawniają się błędy?

JavaScript zgłasza wyjątki lub przeprowadza niejawne przekształcenia typów dopiero w czasie wykonania2. To oznacza, że musimy uruchomić program, aby otrzymać cenną informację o tym, że wykonaliśmy nieprawidłowe działanie. W najlepszym przypadku zachodzi to w czasie testów jednostkowych, w najgorszym – przejawia się w postaci nieprzyjemnego e-maila od użytkownika.

TypeScript zgłasza błędy związane ze składnią i z typami w czasie kompilacji. W praktyce oznacza to, że tego rodzaju błędy pojawiają się w edytorze w czasie pisania kodu. Jest to niesamowite przeżycie dla osób, które nie miały wcześniej okazji programować w językach z typowaniem statycznym i kompilacją przyrostową3.

Niemniej istnieje wiele błędów, których TypeScript nie może wykryć w czasie kompilacji, takich jak: przepełnienie stosu, przerwane połączenia sieciowe czy niepoprawne dane wejściowe, które skutkują zgłoszeniem wyjątku w czasie uruchomienia. Zadaniem TypeScriptu jest wychwycenie w czasie kompilacji wielu błędów, które w przypadku użycia wyłącznie kodu JavaScriptu ujawniłyby się dopiero po uruchomieniu.

Przygotowanie edytora kodu

Skoro wyjaśniliśmy już, w jaki sposób działa kompilator TypeScript Compiler i system typów, pokażemy, jak przygotować edytor do rozpoczęcia pracy z prawdziwym kodem.

Zacznijmy od pobrania edytora kodu, w którym będziemy pisać kod. Lubimy VSCode, ponieważ edytowanie w nim kodu TypeScriptu jest szczególnie wygodne, ale można również użyć Sublime Text, Atom, Vim, WebStorm bądź innego preferowanego edytora. Programiści mają zwykle zdecydowane preferencje dotyczące środowisk IDE, dlatego zostawiamy ci wolny wybór. Jeśli chcesz użyć VSCode, zainstaluj go zgodnie z instrukcjami przedstawionymi na stronie https://code.visualstudio.com/.

Sam kompilator TypeScript Compiler (TSC) jest aplikacją wiersza polecenia napisaną w języku TypeScript4, w związku z tym do jego uruchamiania potrzebny jest NodeJS. Postępuj zgodnie z instrukcjami ze strony https://nodejs.org/en/, aby przygotować NodeJS do działania na swojej maszynie.

W skład środowiska NodeJS wchodzi menedżer pakietów NPM, którego użyjemy do zarządzania zależnościami projektu i zorganizowania kompilacji. Na początku skorzystamy z niego do zainstalowania pakietów TSC oraz TSLint (linter dla TypeScriptu). Zacznijmy od otwarcia terminala i utworzenia nowego folderu, a następnie zainicjalizujmy w nim nowy projekt NPM:

# Utwórz nowy folder

mkdir chapter-2

cd chapter-2

# Zainicjalizuj nowy projekt NPM (postępuj zgodnie z instrukcją)

npm init

# Zainstaluj TSC, TSLint i deklaracje typów dla NodeJS

npm install --save-dev typescript tslint @types/node

tsconfig.json

Każdy projekt TypeScriptu powinien zawierać w katalogu głównym plik o nazwie tsconfig.json. W pliku tsconfig.json projekty TypeScriptu definiują m.in. które pliki powinny być kompilowane, do którego katalogu mają być kompilowane i która wersja JavaScriptu ma być generowana.

Utwórzmy nowy plik o nazwie tsconfig.json w katalogu głównym (touch tsconfig.json)5, a następnie otwórzmy go w edytorze kodu i dodajmy do niego poniższe ustawienia:

{

“compilerOptions”: {

“lib”: [“es2015”],

“module”: “commonjs”,

“outDir”: “dist”,

“sourceMap”: true,

“strict”: true,

“target”: “es2015”

},

“include”: [

“src”

]

}

Przeanalizujmy pokrótce wybrane opcje i ich znaczenie (tabela 2.2):

Tabela 2.2. Opcje tsconfig.json

Opcja

Opis

include

W których folderach TSC ma szukać plików TypeScript?

lib

Jakie API można założyć, że istnieją w środowisku, w którym będzie uruchamiany kod? Dotyczy to takich metod, jak: Function.prototype.bind w ES5, Object.assign, w ES2015 i document.querySelector w DOM.

module

Do jakiego systemu modułów TSC ma kompilować nasz kod (CommonJS, SystemJS, ES2015 itd.)?

outDir

W jakim folderze TSC ma umieścić wygenerowany kod JavaScriptu?

strict

Zastosowanie trybu ścisłego (najbardziej restrykcyjnego) podczas sprawdzania nieprawidłowości w kodzie. Ta opcja wymaga, aby w całym kodzie były stosowane odpowiednie typy. Będziemy używać tego ustawienia we wszystkich prezentowanych w tej książce przykładach i warto stosować je we wszystkich projektach TypeScript.

target

Do jakiej wersji JavaScript ma być kompilowany kod (ES3, ES5, ES2015, ES2016 itd.)?

To tylko kilka wybranych opcji – tsconfig.json oferuje ich dziesiątki i ciągle dodawane są nowe6. W praktyce jednak rzadko dokonujemy zmian, poza dostosowywaniem ustawień module i target ze względu na użycie nowego narzędzia typu module bundler, dodaniem “dom” do lib w przypadku pisania kodu TypeScriptu dla przeglądarki (patrz rozdział 12) lub dostosowywaniem poziomu rygorystyczności podczas migracji istniejącego kodu JavaScriptu do TypeScriptu (patrz „Stopniowa migracja z JavaScriptu do TypeScriptu” na stronie 230). Pełną i aktualną listę wspieranych opcji można znaleźć w oficjalnej dokumentacji na stronie internetowej TypeScriptu (https://www.typescriptlang.org/docs/handbook/compiler-options.html).

Warto mieć świadomość, że choć konfigurowanie TSC przy użyciu pliku tsconfig.json jest wygodne, ponieważ pozwala na wprowadzenie tej konfiguracji do systemu kontroli wersji, większość opcji TSC można również ustawić za pośrednictwem wiersza poleceń. Aby zobaczyć listę opcji dostępnych w wierszu poleceń, należy uruchomić polecenie ./node_modules/.bin/tsc --help.

tslint.json

Projekt powinien zawsze zawierać plik tslint.json z konfiguracją TSLint, definiujący konwencje stylistyczne, jakie mają być stosowane w kodzie (tabulatory czy spacje itp.).

Wykorzystywanie narzędzia TSLint nie jest wymagane, ale zdecydowanie zalecamy stosowanie go we wszystkich projektach TypeScriptu w celu wymuszania spójnego stylu w kodzie. Dzięki temu nie będziemy marnować czasu na sprzeczanie się ze współpracownikami o styl w czasie przeglądów kodu.

Następujące polecenie wygeneruje plik tslint.json z domyślną konfiguracją TSLint:

./node_modules/.bin/tslint --init

Następnie możemy dodać do niej przesłonięcia, które wymuszają preferowany styl. Na przykład nasz plik tslint.json wygląda następująco:

{

“defaultSeverity”: “error”,

“extends”: [

“tslint:recommended”

],

“rules”: {

“semicolon”: false,

“trailing-comma”: false

}

}

Pełną listę dostępnych reguł można znaleźć w dokumentacji TSLint (https://palantir.github.io/tslint/rules/). Można również dodawać niestandardowe reguły lub instalować dodatkowe presety, np. dla ReactJS (https://www.npmjs.com/package/tslint-react).

index.ts

Skoro przygotowaliśmy już pliki tsconfig.json oraz tslint.json, możemy utworzyć folder src zawierający nasz pierwszy plik TypeScriptu:

mkdir src

touch src/index.ts

Struktura folderów w tym projekcie powinna wyglądać następująco:

Otwórzmy plik src/index.ts w edytorze kodu i wpiszmy następujacy kod TypeScriptu:

console.log(‘Hello TypeScript!’)

A następnie skompilujmy i uruchommy kod TypeScriptu:

# Kompiluj TypeScript przy użyciu TSC

./node_modules/.bin/tsc

# Uruchom kod przy użyciu NodeJS

node ./dist/index.js

Jeśli wszystkie kroki zostały prawidłowo wykonane, kod powinien się uruchomić, a w konsoli wyświetlić się pojedynczy napis:

Hello TypeScript!

W taki właśnie sposób przygotowaliśmy i uruchomiliśmy od podstaw nasz pierwszy projekt TypeScriptu. Doskonale!

Ponieważ niektórzy czytelnicy po raz pierwszy mieli okazję konfigurować projekt TypeScriptu od podstaw, omówiliśmy każdy kolejny krok, by zaprezentować wszystkie ruchome elementy. Aby przyśpieszyć ten proces, w przyszłości można zastosować pewne skróty:

Zainstalować ts-node (https://www.npmjs.com/package/ts-node) i użyć go do kompilowania oraz uruchamiania kodu TypeScriptu za pomocą jednego polecenia.

Użyć pomocniczego narzędzia, takiego jak typescript-node-starter (https://github.com/Microsoft/TypeScript-Node-Starter), do szybkiego generowania struktury folderów.

Ćwiczenia

Skoro dysponujesz już skonfigurowanym środowiskiem, otwórz plik src/index.ts w edytorze kodu. Wpisz następujący kod:

let a = 1 + 2

let b = a + 3

let c = {

apple: a,

banana: b

}

let d = c.apple * 4

A teraz umieść kursor nad a, b, c i d, zwracając uwagę, w jaki sposób TypeScript samodzielnie wnioskuje typy zmiennych: a to number, b to number, c to obiekt o określonym kształcie, d to również number (rysunek 2.3).

Rysunek 2.3. Wnioskowanie typów w TypeScripcie

Poeksperymentuj z kodem. Sprawdź, czy potrafisz:

Sprawić, że TypeScript wyświetli czerwoną falistą linię, gdy zrobisz coś nieprawidłowo (co nazywamy „zgłaszaniem błędu TypeError”).

Przeczytać TypeError i spróbować zrozumieć, co on oznacza.

Naprawić błąd TypeError, aby czerwona falista linia zniknęła.

Jeśli jesteś ambitny, spróbuj napisać kod, dla którego TypeScript nie będzie potrafił wywnioskować typu.

Przypisy

Przedmowa

1 Oto przykład dla osób niezaznajomionych z językiem JavaScript. Gdy masz obiekt o i chcesz dodać do niego właściwość k o wartości 3, możesz bezpośrednio go zmodyfikować (o.k = 3) lub zastosować zmianę na o, co prowadzi do utworzenia nowego obiektu (let p = {...o, k: 3}).

1. Wprowadzenie

1 Zależnie od tego, który język z typowaniem statycznym wykorzystujesz, „nieprawidłowe” może mieć różne znaczenie – od programów, które nagle przerywają pracę, po takie, których działanie nie ma sensu.

2 Jeśli nie do końca rozumiesz, co oznacza „poziom typów”, nic nie szkodzi. To zagadnienie omówimy szczegółowo w kolejnych rozdziałach.

2. TypeScript: Ogólny obraz

1 Języki programowania obejmują całe spektrum: JavaScript, Python i Ruby wnioskują typy w czasie wykonania, Haskell i OCaml wnioskują i sprawdzają brakujące typy w czasie kompilacji, Scala i TypeScript wymagają jawnego adnotowania niektórych typów i wnioskują oraz sprawdzają pozostałe w czasie kompilacji, natomiast Java i C wymagają jawnych adnotacji dla prawie wszystkiego, co sprawdzają w czasie kompilacji.

2 Tak dla jasności: JavaScript wskazuje błędy składni i kilka wybranych pomyłek (takich jak deklaracje const o powtarzających się nazwach w tym samym zakresie) po sparsowaniu programu, ale przed jego uruchomieniem. Gdy parsujemy kod JavaScriptu w ramach procesu budowania (np. przy użyciu kompilatora Babel), możemy ujawniać te błędy w czasie budowania.

3 Języki z kompilacją przyrostową umożliwiają szybką rekompilację po dokonaniu małej zmiany, ponieważ nie wymagają rekompilacji całego programu (łącznie z częściami, które nie uległy zmianie).

4 Dzięki czemu TSC zalicza się do mistycznej grupy kompilatorów nazywanych kompilatorami self-hosting, czyli kompilatorami, które same się kompilują.

5 W tym ćwiczeniu własnoręcznie tworzymy plik tsconfig.json. W przyszłych projektach TypeScriptu do jego automatycznego wygenerowania można użyć wbudowanego w TSC polecenia inicjalizacji: ./node_modules/.bin/tsc --init.

6 Aby komunikaty o błędach były wyświetlane w polskiej wersji językowej, można użyć opcji: „typescript.locale”: „pl”. Warto dodać ją również do pliku ustawień VSCode (settings.json), aby zobaczyć polską wersję także w edytorze kodu (przyp. tłum.).