Kurs TypeScript – część 3

Kontynuuję serię wpisów na temat TypeScript. Pisząc poprzedni post złapałem się na tym, że dobijałem już do blisko 1800 słów. Uznałem, że to zdecydowanie za dużo jak na jeden wpis i stąd tak szybko kolejna część 😉 W tym odcinku mówię o zaawansowanych typach, aliasach i literałach. Dodatkowo poruszam również temat inferencji typów. Zapraszam do czytania 🙂

Zakładam, że czytelnicy są zaznajomieni z JavaScriptem, a w szczególności z konceptami dodanymi w ECMAScript 2015 takimi jak class oraz letconst. Jeśli jednak coś jest niejasne to chętnie odpowiem na pytania w komentarzach.

Zaawansowane typy

W poprzednim wpisie przy okazji przykładu z tablicą zwierząt, użyłem czegoś co nazwałem union type. Dla przypomnienia wyglądało to tak:

const animals:Array<Horse|Dog|ShibaInu|Poodle> = [];  

Jest to jeden z tzw. typów zaawansowanych. Omówmy je teraz szybko:

Union type

Union type pozwala na opisanie typu, który jest jednym typem lub drugim typem. Przykładowo możemy stworzyć funkcję, która jako argument przyjmuje number lub Date:

function formatDate(date:number|Date) {  
    if (typeof date === "number") {
        // tutaj TypeScript już wie, że data jest liczbą!
        date = new Date(date);
    }
    …
}

Intersection type

Intersection type jest blisko związany z union type, ale pozwala na opisanie typu, który ma cechy kilku typów na raz. Najczęściej wykorzystywany jest z interfejsami. Korzystając z interfejsów z poprzedniej części, wyobraźmy sobie, że chcemy stworzyć funkcję, która oczekuje obiektu będącego na raz SerializableDrawable:

function mojaFunkcja(obiekt:Serializable & Drawable) {  
    // obiekt na pewno ma metody toJSON i draw!
}

Aliasy typów

TypeScript pozwala na definiowanie aliasów typów. Możemy na przykład zdefiniować typ Name, który będzie stringiem:

type Name = string;

class User {  
    firstName:Name;
}

Dzięki temu potencjalna zmiana jednego typu na drugi będzie łatwiejsza. Dodatkowo poprawia to również czytelność kodu i łatwość jego tworzenia. Spójrzmy na przykład:

class Process {  
    flag: boolean;
}

Intuicyjnie, pole o nazwie flag mogłoby być typu boolean, enum, number lub string i każdy z tych typów mógłby mieć sens. Możliwe jest również, że kiedyś w przyszłości będziemy potrzebować zmienić boolean na string (z powodu daleko idącej refaktoryzacji kodu). Jeśli pole flag zadeklarujemy po prostu jako boolean, taka zmiana będzie znacznie trudniejsza. Oprócz klasy będziemy musieli prawdopodobnie również zmodyfikować funkcje, które będą miały na stałe wpisany typ boolean:

isProcessFlagValid(flag:boolean) {  
    …
}

Dobrym rozwiązaniem tego problemu jest stworzenie nowego typu dla tego pola:

type ProcessFlag = boolean;

class Process {  
    flag: ProcessFlag;
}

isProcessFlagValid(flag:ProcessFlag) {  
    …
}

Dzięki temu nie musimy pamiętać dokładnie jakiego typu jest to pole w klasie, a w razie potrzeby jego zmiany, łatwo znajdziemy wszystkie miejsca, w których wymagana jest modyfikacja, po prostu szukając odwołania do typu ProcessFlag.

Alias funkcji

Możliwe jest również zdefiniowanie typu oznaczającego funkcję. Jest to bardzo przydatne przy opisywaniu definicji callbacków przekazywanych do funkcji. Wyobraźmy sobie, że tworzymy bibliotekę, w której jedna z funkcji oczekuje, że inny programista przekaże jako argument funkcję, która jako argument przyjmuje obiekt typu User i zwraca true lub false jeśli użytkownik jest poprawny.

// nasza biblioteka
type UserCallback = (user:User) => boolean;

function fetchUser(callback:UserCallback) { … }

// kod użytkownika
function fetchUserCallback(user:User) {  
    if (user.name === 'Michal') {
        return true;
    }
    return false;
}

fetchUser(fetchUserCallback);  

Dzięki zadeklarowaniu typu callback w powyższy sposób, jeśli użytkownik spróbuje przekazać nieprawidłową funkcję jako argument to otrzyma błąd:

// blad!
fetchUser((user:User) => {  
    // zapomnialem zwrocic true lub false
});

String Literal Type

Często zdarza mi się potrzeba zadeklarowania tego, że funkcja jako argument może przyjąć nie tyle typ, co konkretne wartości. Przykładowo tworzymy funkcję, która pobiera pewne rekordy z bazy danych. Chcemy dać użytkownikom możliwość grupowania tych danych po sekundach, minutach, godzinach lub dniach. Rozwiązaniem, które może przyjść do głowy to zadeklarowanie, że funkcja przyjmuje string, a następnie sprawdzenie czy ten string ma odpowiednią wartość:

function groupRecords(groupBy:string) {  
    if (groupBy === "second" || groupBy === "minute" || groupBy === "hour" || groupBy === "day") {
        …
    } else {
        // blad!
    }
}

Istniej jednak lepsze rozwiązanie od tego. Zdefiujmy sobie typ GroupBy:

type GroupBy = 'second'|'minute'|'hour'|'day';

function groupRecords(groupBy:GroupBy) {  
    …
}

Dzięki temu kompilator sam sprawdzi (w miarę możliwości!), czy podana wartość jest prawidłowa. String literal type świetnie sprawdzi się też jako flaga wspomniana w poprzednim akapicie.

Inferencja typów

O inferencji typów wspominałem już w swoim innym wpisie na temat TypeScripta: TypeScript z node.js?. Ponownie wykorzystam przykład z tamtego wpisu:

function fn(b:boolean) {  
    if (b === true) {
        return 1;
    } else {
        return 2;
    }
}

Ta funkcja zwraca liczbę i jest to ewidentne. TypeScript również jest tego pewien i dlatego nie musimy tutaj podawać zwracanego typu. TypeScript inferuje, że jest to number:

const liczba:number = fn(true); // dziala!  

Możemy pójść nawet o krok dalej. Skoro fakt, że fn zwraca lizbę jest oczywisty, to czy w ogóle konieczne jest deklarowanie liczba:number? Nie!

const liczba = fn(true); // dziala!  

Ponownie TypeScript inferuje, że zmienna liczba jest typu number. Ten kod oraz poprzedni są sobie całkowicie równoważne.

W sytuacjach, które są dwuznaczne TypeScript wyświetli błąd i zmusi do zadeklarowania odpowiedniego typu:

function fn2(b:boolean):string|number {  
    if (b === true) {
        return 1;
    } else {
        return 'lol';
    }
}

Bez deklaracji string|number otrzymalibyśmy błąd:

No best common type exists among return expressions.

Inferencja typów działa również gdy od razu przypisujemy do zmiennej lub stałej wartości:

const tab1 = [0, 1, 'lel']; // Array<number|string>  
const tab2 = [0, null]; // Array<number>  
const tab3 = [new Dog('leszek'), new Horse('rafal')]; // Array<Dog|Horse>  

Dwie pierwsze linijki są chyba oczywiste. Jednak w ostatnim przypadku (odwołuję się tutaj do kodu z poprzedniego wpisu) klasy DogHorse mają wspólną klasę bazową Animal, a więc moglibyśmy przecież oczekiwać, że tablica tab3 będzie typu Animal! Tak się jednak nie dzieje i TypeScript jasno informuje o tym w swojej dokumentacji. Aby otrzymać taki efekt w tablicy musiałaby się znaleźć instancja klasy Animal – w przeciwnym wypadku musimy ręcznie zadeklarować typ. To wszystko dla naszego dobra, uwierzcie mi na słowo 🙂

const tab3:Array<Animal> = [new Dog('leszek'), new Horse('rafal')]; // Array<Animal>  

Dzięki rozbudowanemu mechanizmowi inferencji typów kod staje się o wiele bardziej zwięzły i prostszy do pisania bez utraty zalet statycznego typowania. Wiele długich i formalnych definicji możemy po prostu pominąć. Inferencja typów jest elementem praktycznie każdego nowoczesnego języka programowania, między innymi C#, Go, C++, Haskell, Swift czy Rust.

Podsumowanie

I to by było na tyle w tym wpisie! Dowiedzieliśmy się całkiem sporo na temat typów zaawansowanych: union typeintersection type. Ponadto nauczyliśmy się definiować aliasy typów oraz funkcji. Doceniliśmy również zwięzłość kodu, jaką daje nam inferencja typów 🙂

W kolejnym wpisie z serii wykorzystamy zdobytą wiedzę i spróbujemy przepisać prosty widget napisany w JS na TypeScript. Zachęcam do komentowania i zadawania pytań!

Nawigacja po kursie:
  1. Po co TypeScript?
  2. TypeScript – część 1
  3. Kurs TypeScript – część 2
  4. Kurs TypeScript – część 3

Nie wysyłamy spamu, tylko wartościowe informacje. W każdej chwili możesz się wypisać klikając „wypisz się” w stopce maila.

Subscribe
Powiadom o
guest
24 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
CodeFinger
CodeFinger
4 lat temu

Dzięki za kurs 🙂 Kawał dobrej roboty!

Michał
Michał
4 lat temu

Dzięki bardzo za artykuły o typescripcie. Tego potrzebowałem!
Kiedy kolejna część?

Romek Kruczykowski
Romek Kruczykowski
1 rok temu

dłuuuugi ten tydzień 🙂 kiedy 4 cześć 😀

Szymon
Szymon
3 lat temu

Czekamy!

Grzegorz
Grzegorz
1 rok temu

Ja bym reflektował bardzo na React + TypeScript. Właśnie mnie to męczy :/

lukaszzz6923
lukaszzz6923
1 rok temu
Reply to  Grzegorz

React/Redux + TypeScript 😀 !

Marcin Kalinowski
Marcin Kalinowski
1 rok temu

Jeśli używasz zmiennej o konkretnym typie to po co używasz operatora === ? Przecież służy on do upewnienia się że typem jest boolean a to jest pewne bo sam to zaznaczyłeś w argumencie funkcji.

Marcin Kalinowski
Marcin Kalinowski
1 rok temu

Do porównania wartości służy ==. Operator === porównuje zarówno wartość jak i typ zmiennej.

Marcin Kalinowski
Marcin Kalinowski
1 rok temu

Rozumiem, teraz to logiczne. Dzięki za odpowiedź ☺

Fedox
Fedox
1 rok temu

Co mi po tym, że pisze w typescriptcie skoro i tak będzie to skompilowane do JS. Jak pokazują statystyki brak typów wcale nie zwiększa ilości błędów, ilość błędów w typescript jak i javascript jest niemal taka sama. Rzeczą która mogła by mnie przekonać byłaby np. automatyczna validacja wejścia użytkownika – nie musiałbym pisać validatora bo byłby on automatycznie przygotowany na podstawie moich typów. Całą resztę elementów można uzyskać również w czystym JS. Wystarczy dobry edytor i trochę dodatkowych rzeczy np. eslint a będziemy w stanie pozbyć się tych samych błędów którym zapobiega typescript

Jakub Sarnowski
Jakub Sarnowski
1 rok temu
Reply to  Fedox

Poza komentarzem @Miszy:disqus polecam sprawdzić także artykuł Erica Elliotta w temacie zwrotu z kosztów używania TS: https://medium.com/javascript-scene/the-typescript-tax-132ff4cb175b

Jaari
Jaari
1 rok temu

Inferencja typów jest trochę zastanawiającym rozwiązaniem. Wydaje mi się, że używając jej, zamiast pisać po prostu typ, kod staje się mniej czytelny.

Na przykład:
const tab1 = [0, 1, ‚lel’];

wydaje mi się mniej czytelny, niż

const tab1:Array = [0, 1, ‚lel’];

Jak później chcę wykorzystać gdzieś stałą tab1, to w 1. przypadku muszę się głowić, co za typ zostanie tam przypisany przez inferencję typów.

W 2. przypadku piszę raz, przy deklaracji stałej, jaki ma typ i później, przy wykorzystaniu jej, wystarczy, że spojrzę, jaki ma ona typ i już wiem, jak mogę ją wykorzystać.

Nie wiem, czy dobrze zrozumiałem zagadnienie, może coś poknociłem, wtedy proszę o sprostowanie i wyjaśnienie, dlaczego (poza trochę mniejszą ilością kodu) używanie inferencji typów jest lepsze od samodzielnego definiowania tych typów.

Przecież „Nigdy nie spojrzałem na kod i nie pomyślałem: »chciałbym mieć teraz mniej informacji o typach«” 🙂

eluwina
4 miesięcy temu

typie co z tą książką o TS???