Menu Zamknij
19 grudnia 2021

TypeScript - szkolenie autorskie.

Czym jest TypeScript?

TypeScript jest językiem programowania stworzonym w modelu open-source. TypeScript jest nadzbiorem JavaScriptu. Zespół odpowiadający za jego utrzymanie i wprowadzenie w nim nowych funkcjonalności pracuje w Microsoft. Składnia języka TypeScript jest prawie identyczna z JavaScriptem, z tym że TypeScript dodaje kilka nowych featurów, jak: statyczne typowanie, interfejsy

Co ciekawe, każdy kod napisany w JS możemy skopiować do pliku z rozszerzeniem .ts i kod ten będzie działać poprawnie, jednak nie od razu. Kompilator TypeScriptu wprowadza pewne obostrzenia i dopiero gdy je zluzujemy, kod z JS może zostać poprawnie uruchomiony. Dzieje się tak dlatego, iż TypeScript jest kompilowany do czystego JSa. 

Jeśli jednak skompilujemy kod ts do pliku js i uruchomimy go w przeglądarce, to przeglądarka nie będzie miała pojęcia o typach, bo one nie istnieją w ECMAScript. 

Microsoft jednak udostępnia kompilator online, który pozwala przetestować jak kod TS jest kompilowany do kodu JS, możesz go przetestować tutaj

Dlaczego powinniśmy korzystać z TypeScript?

Skoro TS kompiluje się do JS, to zastanawiasz się na pewno co sprawia, że warto używać TSa i co sprawia, że korzystają z niego takie renomowane marki jak: Google, Netflix, Facebook i wiele, wiele innych. 

Otóż dzięki TS, podczas pisania kodu, nie musisz przykładowo zastanawiać się czy funkcja przyjmuje jako parametr tekst, liczbę, czy autor obsługuje oba przypadki. Kompilator TS od razu podpowiada jakiego typu jest oczekiwany parametr. 

Jednak to nie wszystko! Statyczne typowanie to dopiero początek przygody. Ułatwia to utrzymanie kodu, pracę w dużych projektach i aplikacjach oraz ich rozwój.

TypeScript dostarcza też interfejsy, generyki, modyfikatory dostępu, abstrakcje, czyli wszystko to czego oczekujemy od programowania obiektowego. 

Dzięki temu programistom C# czy Javy jest bardzo łatwo przesiąść się na TS. Również możliwość wykorzystywania TSa w React i Node.js podbija jego popularność i użyteczność.

Elementy TypeScriptu

TypeScript pod spodem składa się z:

  1. języka programowania,
  2. serwera języka - program dodaje do Twojego IDE autouzupełnianie, typy w locie, dodaje podpowiedzi do kodu.
  3. kompilatora - jest to program, który sprawdza typy i składnię. Zamienia kod TS na kod JS, używany jest przez serwer języka, bądź podczas budowania naszej aplikacji.

Instalacja w projekcie

Do zainstalowania TS w projekcie będziemy potrzebowali:

  • NodeJS,
  • Edytora kodu np. Visual Studio Code,

Po zainstalowaniu narzędzi musimy utworzyć katalog na projekt i otworzyć go w edytorze kodu. W moim przypadku folder ten będzie się nazywał hello-typescript. Następnie otwieramy terminal i inicjujemy projekt komendą:

npm init -y 

Dzięki niej, do naszego katalogu, dodamy package.json, który jest plikiem konfiguracyjnym projektu dla npm (w nim znajduje się “opis” zewnętrznych zależności projektu). TypeScript możemy zainstalować globalnie lub lokalnie (tylko dla projektu). 

Aby zainstalować Typescript lokalnie użyjemy komendy:

npm i typescript --save-dev


Dependecies
Powstanie katalog node_modules z plikami TS, zależność ta powinna pojawić się w sekcji devDependencies w package.json:

Teraz użyj komendy:

npx tsc --init

Komenda ta doda do projektu plik konfiguracyjny dla TS (tsconfig.json). W pliku tym możemy ustawić, do jakiej wersji ECMAScript plik ma się kompilować, jakie są obostrzenia dotyczące typów i wiele innych, które są opisane w dokumentacji TS.

Napiszmy tradycyjne hello world w TypeScript. Stwórz plik hello.ts.

const hello:string = "Hello world";

console.log(hello)

Żeby skompilować kod, musimy użyć komendy:

npx tsc

Komenda ta stworzy nam plik hello.js, w którym jest kod JS, który powstał w wyniku kompilacji do js. 

Następnie za pomocą komendy...

node hello.js

... możemy uruchomić program.

Wówczas na konsoli pojawi się "Hello World".

Programista wszędzie szuka automatyzacji, dlatego też powyższy proces również możemy zautomatyzować. Z pomocą przychodzi nam komenda:

npx tsc --watch

Dzięki niej kompilator TS będzie nasłuchiwał zmian w naszym kodzie i od razu przekompiluje go w przypadku każdej modyfikacji. 

Dzięki takiemu rozwiązaniu nie będziesz zmuszony do ciągłego rekompilowania zmian przy nawet najdrobniejszych zmianach.

Zalety

Na ochłodzenie Twoich zwojów mózgowych 🙂 Wtrącając nieco teorii do praktyki, przedstawiam poniżej zalety wynikające z tworzenia kodu w TypeScript. 

  • Łatwiejsze debugowanie -  w JS o błędach w kodzie dowiadujemy się dopiero podczas uruchomienia kodu i podczas testowania aplikacji, TypeScript pozwala znaleźć bugi już podczas pisania kodu.
  • Przejrzystość w kodzie - TS pozwala pisać kod łatwiejszy do zrozumienia niż kod JSowy. Łatwiej jest się domyślić, za co odpowiada funkcja, zmienna, bo oprócz nazwy znany jest nam również oczekiwany typ danej.
  • Kompilator TS automatycznie usuwa luki pomiędzy wersjami JS. Dzięki temu możemy korzystać z najnowocześniejszych funkcjonalności i jednocześnie nie obawiać się o niepoprawne działanie kodu na innych starszych przeglądarkach. 

Wykorzystywanie typów

  • Typ w TS podajemy według wzoru: let/const zmienna:typ= …
  • Typy w TS podajemy z małej litery.

Typy number, string oraz boolean

Najczęściej wykorzystywanymi typami prymitywnymi w JS są:

string, number i boolean

Poniższy przykład demonstruje ich użycie.

const hello:string = "Hello world";
const isTrue:boolean = false;
const age:number = 15;

Spróbuj podać do zmiennej age wartość tekstową “test”.

String unassignable

Kompilator TSa zwraca uwagę programiście, że popełnia błąd i niemożliwe jest przypisanie stringa do zmiennej typu number. Na tym właśnie polega statyczne typowanie w TS i większa przejrzystość pisanego kodu. 

Typowanie oraz inferencja typów

Poprzez inferencję, TypeScript potrafi automatycznie rozpoznać, jaki typ jest przypisany do zmiennej. 

Dzieje się to m.in podczas przypisania wartości do zadeklarowanej zmiennej, ustawiania wartości domyślnej, zdefiniowania typu zwracanego z funkcji.

String not to boolean

TypeScript bez jawnego otypowania przez programistę wie, że zmienna isTrue jest typu boolean i nie możemy do niej przypisać stringa. W JS istniałaby taka możliwość.

Typ Object oraz Array

Typowanie tablic można wykonać na dwa sposoby:

let list:number[] = [1,2,3]
let names:Array<string> = ["Adam","Alfred","Alojzy"]

Możemy też typować zmienne w poniższy sposób:

Alternative typing

Mamy możliwość stworzenia również typu jako tablicy obiektów:

Human object

Używanie tupli (krotek) oraz enum

Typ tuple pozwala nam zdefiniować tablicę typów. 

Załóżmy, że chcemy otypować zmienną będącą parą string i boolean (zawsze w tej samej kolejności).

Adam not assignable

Przy próbie przypisania zmiennych na odwrót TS podkreśla to jako błąd. A to dzięki stworzeniu krotki, która składa się właśnie z dwóch elementów w kolejności string, boolean.

Enum jest typem pozwalającym na nadanie przyjaznych nazw dla zbioru liczb. Dzięki temu możemy tworzyć własny typ, w obrębie którego zdefiniujemy własne “podtypy” z przypisaną do nich wartością.

Domyślnie składowe enuma inicjalizowane są kolejnymi całkowitymi, licząc od 0, ale możesz to zmienić przypisując wartość dla jednej ze składowych.

Zauważ, że kompilator podpowiada wówczas, że wartość liczbowa dla koloru BLUE to 2. Jeżeli przypiszemy do RED wartość 3, reszta również zostanie zmieniona.

Identifier expected-1

Po zmianie, dla koloru BLUE obecna wartość liczbowa to 5.

Identifier expected-2

Typ "I Don't Care" czyli Any

Typ any pozwala na przypisanie jakiegokolwiek typu do zadeklarowanej zmiennej.

List of types

Unknown object

 

 

 

 

Jak widać do zmiennej z typem any, możemy przypisać, co nam się podoba i TS nie zwraca nam żadnych uwag.

Podobnym do any jest również typ unknown (tzw. typ nieznany), który również umożliwia nam przypisanie wartości dowolnego typu do danej zmiennej. W przeciwieństwie jednak do any, nie możemy na zmiennej “typu nieznanego” wywoływać żadnej metody. 

Typ Union

Może się zdarzyć, że będziesz korzystać lub tworzyć funkcje, które będą przystosowane do obsługi kilku typów jednocześnie.

function marginLeft(value:string | number) {}

Powyższa funkcja przyjmuje argument typu union. Oznacza to tyle, iż zmienna value może być typu string lub number. Omawiany typ sprawdza się w momencie, gdy przewidujemy sytuację, gdzie dana zmienna nie będzie przechowywała tylko i wyłącznie wartości jednego typu. 

Typ Literal

Typ literal pozwala na przypisanie określonych wartości do zmiennej, a następnie na wykorzystywanie takiej zmiennej w celu ustalenia możliwych do wystąpienia wartości w określonej danej.

Not assignable

Poniżej tworzymy literał cases, który mówi nam, iż każda zmienna, do której zostanie przypisany taki literał, będzie mogła przechowywać tylko i wyłącznie wartość “add” lub “substract” lub “remove”.

 

 

Expression expected

W typie literal możemy również podać wartości oczekiwane o różnych typów - może to być mieszanka string, number, boolean i typów zdefiniowanych przez nas samych. TypeScript nie pozwoli wtedy wprowadzić wartości innych niż zdefiniowanych przez nas.


Tworzenie własnych typów

type Kid = {
  name: string;
  parentName: string;
};

type myType = {
  name: string;
  age: number;
  kids: Kid[];
};

const human: myType = {
  name: "Max",
  age: 33,
  kids: [
    { name: "Jolanta", parentName: "Max" },
    { name: "Andzelika", parentName: "Max" },
  ],
};

W powyższym przykładzie stworzyłem dwa typy Kid oraz myType

W typie myType użyłem stworzonego wcześniej typu - Kid. 

Na końcu zdefiniowałem obiekt human z typem myType i uzupełniłem wszystkie niezbędne pola (zgodnie z tym, jak zostały one określone na poziomie myType).

Funkcja jako typ

Do funkcji możemy podać jako zwracaną wartość typ prymitywny, typ obiektu, tablicę, stworzony przez nas typ lub typ void.

Typ void oznacza, że funkcja nic nie zwraca.

type Kid = {
name: string;
parentName: string;
};

type Kids = Kid[];

type myType = {
name: string;
age: number;
children: Kid | Kids;
};

function humanCreator(name: string, age: number, children:Kid|Kids):myType{
return {
name,
age,
children
};
};

const human = humanCreator("Amu",23,{name:"Tatiana",parentName:"Amu"})
const human2 = humanCreator("Amu", 23, [{ name: "Tatiana", parentName: "Amu" }, { name: "Tatiana2", parentName: "Amu" }]);

Powyższy przykład różni się od poprzednika tym, iż dodałem typ Kid oraz Kids. Drugi z nich jest tablicą, aby pokazać, że możemy również tworzyć tablice z naszych typów. 

Następnie nadałem typ zwracany dla funkcji humanCreator (myType) i otypowałem jej parametry. 

Podczas pisania tej funkcji TypeScript, jak nasz kompan podczas sesji pair-programming podpowiadał, czego jeszcze brakuje w obiekcie zwracanym z funkcji.

Klasy i interfejsy

Co to są klasy i jak je tworzyć wiesz już z wcześniejszych szkoleń, więc przejdę od razu do ciekawszych funkcjonalności, jakie dostarcza nam TypeScript. 

Modyfikatory dostępu "public" oraz "private"

W TypeScript każde pole klasy jest domyślnie publiczne. Pole klasy oznaczone jako prywatne nie jest dostępne z zewnątrz.

Class example


W powyższym przykładzie, przy próbie odwołania się do zmiennej prywatnej spoza kontekstu klasy, TypeScript zwraca błąd, że właściwość name jest prywatnym polem i może być dostępne tylko w klasie Animal.

Modyfikator "read-only"

Dzięki użyciu słowa kluczowego readonly możemy tylko odczytać pole. Pole to musi zostać zainicjowane w konstruktorze. 

Modyfikator readonly ma też zastosowanie w interfejsach, o których opowiem poniżej

Class read-only


Klasy abstrakcyjne

Klasy abstrakcyjne nie mogą mieć swojej instancji, są to klasy bazowe dla klas dziedziczących po niej. Aby zdefiniować klasę abstrakcyjną, musimy użyć słowa kluczowego abstract.

W powyższym przykładzie widać, że TS zwraca błąd przy próbie stworzenia instancji klasy abstrakcyjnej. 

To jest właśnie zasadnicza różnica w stosunku do zwykłych klas bazowych - klasy abstrakcyjne uniemożliwiają utworzenie instancji takiej klasy, służy tylko i wyłącznie do przekazywania “cech” potomkom. 

Cannot create instance

Co to są interfejsy i jak je tworzyć?

Interfejs jest specjalnym rodzajem klasy abstrakcyjnej i różni się od niej tym, iż w interfejsie możemy umieszczać tylko i wyłącznie deklaracje zmiennych i metod (bez definiowania ich ciał). 

W interfejsach, podobnie jak własnoręcznie definiowanych typach, możemy używać opcjonalnych właściwości oraz słowa kluczowego readonly.

interface IPoint {
    x:number;
    y:number;
    setPoint: (x:number,y:number) = >void
}

class Point implements IPoint {
  x: number;
  y: number;
  constructor(x: number, y: number){
      this.x=x;
      this.y=y;
  }
  setPoint(x:number,y:number){
 
  }
}

Klasa Point dziedziczy po interfejsie IPoint. Po interfejsach dziedziczymy, wykorzystując słowo kluczowe implements. 

Dodatkowo interfejsy przydają się w momencie, gdy chcemy zaimplementować multidziedziczenie. Standardowo klasa może dziedziczyć tylko po jednej klasie bazowej. W przypadku natomiast dziedziczenia po interfejsach, możemy określać niezliczoną ilość rodziców (interfejsów).

Typy generyczne

Podczas wytwarzania oprogramowania, musimy skupić się na budowaniu komponentów, które nie tylko będą działały, ale też będą dawały możliwość użycia ich w kilku miejscach. 

Do tworzenia reużywalnych komponentów służą abstrakcje i typy generyczne. Typ generyczny pozwala na zdefiniowanie komponentu, który będzie działał niezależnie od wariantu lub podanego mu typu.

function genericEx<T>(arg: T): T{
    return arg;
}

const output1 = genericEx<string>("hello")
const output2 = genericEx<number>(1);
const output3 = genericEx<number[]>([1,2,3]);

Funkcja genericEx w < > przyjmuje generyczny typ T. Podczas wywoływania takiej funkcji, możemy dynamicznie określać typ argumentu, z jakim ta funkcja może być wywołana. Dzięki temu jednej funkcji używamy na wiele sposobów. 

W TypeScript mamy również możliwość tworzenia klas generycznych, tak jak na przykładzie poniżej:

class KeyValuePair<T,U>{
    private key: T;
    private value: U;

    constructor(key:T,value:U){
        this.key = key;
        this.value = value;
    }

    setKeyValue(key:T, value:U):void{
        this.key = key;
        this.value = value;
    }
}
const keyPair1 = new KeyValuePair<string,number>("abc",1);
const keyPair2 = new KeyValuePair<string, string>("abc", "cde");

Zdefiniowałem powyżej generyczną klasę z dwoma typami generycznymi, a następnie zdefiniowałem jej dwie różne instancje, z różnymi typami.

TypeScript w React

Aby stworzyć nowy projekt z TypeScript w React należy użyć komendy:

npx create-ract-app myapp --template typescript

Poznanie typów w React

Typów w React używamy tak samo jak do tej pory. A to dlatego, że React jest po prostu frameworkiem JSa i nie wprowadza żadnych gruntownych zmian we flow pisania kodu. 

Natomiast pokażę poniżej kilka przykładów jak używać TSa z hookami i komponentami. Zauważ, że wszystkie hooki przyjmują typ generyczny.

Hook useState

const [state, setstate] = useState<number | undefined>()
const [state1, setstate1] = useState<number[]>([])

interface StateObject {
    key:string;
    value?:number
}

const [state3, setstate3] = useState<StateObject>({key: "abc"})

Hook useContext

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('dark');

const App = () => (
  <ThemeContext.Provider value="dark">
    <MyComponent />
  </ThemeContext.Provider>
)

const MyComponent = () => {
  const theme = useContext(ThemeContext);
  return <div>The theme is {theme}</div>;
}

Hook useRef

const MyInput = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  return <input ref={inputRef} />;
};

Hook useReducer

interface State {
  value: number;
}

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "incrementAmount"; amount: number };

const counterReducer = (state: State, action: Action) => {
  switch (action.type) {
    case "increment":
      return { value: state.value + 1 };
    case "decrement":
      return { value: state.value - 1 };
    case "incrementAmount":
      return { value: state.value + action.amount };
    default:
      throw new Error();
  }
};

const [state, dispatch] = useReducer(counterReducer, { value: 0 });

dispatch({ type: "increment" });
dispatch({ type: "decrement" });
dispatch({ type: "incrementAmount", amount: 10 });

// TypeScript compilation error
dispatch({ type: "invalidActionType" });

TypeScript w komponencie

export const Hello = ({text,value}:{text:string,value:number}) => {
    return (
        <div>
            {text}
            {value}
        </div>
    )
}

Migracja projektu React do React + TS

Aby zmigrować projekt należy podjąć następujące kroki:

  1. Dodać TypeScript do projektu:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

      2. Wygenerować plik tsconfig.json:

npx tsc --init

      3. Konwersja plików

W komponentach zmieniamy rozszerzenie na z .jsx na .tsx, rozszerzenie plików js zmieniamy na ts.

      4. Zwiększenie pokrycia TS

W pliku tsconfig.json możemy włączyć zasady:

“noImplicitAny”: true,
“strictNullChecks”: true,
“noImplicitThis”: true,
“alwaysStrict”: true

Teraz musimy otypować całą aplikację. Może się jednak okazać, w tym momencie nie wiemy, jak otypować dane zmienne, ale musimy szybko sprawić żeby aplikacja się kompilowała. Oczywiście możemy użyć przy adnotacji słowa kluczowego any, jednak może to znacząco zmniejszyć czytelność naszego kodu. Poza tym trzymanie się tylko jednej konwencji - typowania zmiennych tylko przez any - jest pewnego rodzaju antywzorcem.

Wówczas, aby pokazać, że rozwiązanie jest jedynie tymczasowe, można zastosować poniższy zapis:

export type FixMePls = any

Dzięki temu zamiast any, stosujemy typ FixMePls i doprowadzamy do kompilowania się aplikacji. 

Później za pomocą lupki możemy znaleźć wszystkie miejsca zawierające nasz typ FixMePls i otypować zmienne w wolnym czasie, tak aby adnotacja dotyczyła już konkretnych typów. 

W niektórych przypadkach przydaje się też trick z dopisaniem:

@ts-ignore

przy podanym errorze kompilacji.  

Jeżeli chcemy być jeszcze bardziej restrykcyjni w tsconfig.json możemy włączyć reguły:

"noUnusedLocals": true
"noUnusedParameters": true
"noImplicitReturns": true
"noFallthroughCasesInSwitch": true

Powyższa konfiguracja uniemożliwi nam w kodzie:

  • definiowanie nieużywanych nigdzie zmiennych
  • tworzenie nieużywanych w funkcji/metodzie parametrów
  • zwracanie niejawnych wartości z funkcji/metody
  • wyłączenie opcji “fall through” wewnątrz wyrażenia switch() case.

Świętowanie

No i najważnieszy punkt migracji Reacta do TSa - celebrowanie sukcesu 😉 To wszystko! 

Podsumowanie

Powyższy przewodnik ma ułatwić Ci wkroczenie w świat TypeScriptu i uniknięcie pułapek z nim związanych. Nauczyłeś się nie tylko podstaw, ale i zaawansowanych funkcjonalności tego języka oraz pokazałem Ci jak przeprowadzić migrację projektu. Pora na praktykę. Powodzenia!

Sprawdź również nasz system mentorowania i outsourcowania specjalistów
  • Wyznaczona ścieżka od A do Z w kierunku przebranżowienia
  • Indywidualnie dostosowany materiał pod ucznia
  • Code Review ze strony mentora
  • Budowa portfolio
  • Intensywna praca z materiałami szkoleniowymi