Spis treści
- 1. Ogólnie o TypeScript
- 2. Instalacja TS
- 3. Typy
- 4. Klasy i interfejsy
- 5. Typy generyczne
- 6. TypeScript w React
Podziel się wpisem ze znajomymi!
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:
- języka programowania,
- serwera języka - program dodaje do Twojego IDE autouzupełnianie, typy w locie, dodaje podpowiedzi do kodu.
- 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
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”.
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.
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:
Mamy możliwość stworzenia również typu jako tablicy obiektów:
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).
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.
Po zmianie, dla koloru BLUE obecna wartość liczbowa to 5.
Typ "I Don't Care" czyli Any
Typ any pozwala na przypisanie jakiegokolwiek typu do zadeklarowanej zmiennej.
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.
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”.
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 = { type Kids = Kid[]; type myType = { function humanCreator(name: string, age: number, children:Kid|Kids):myType{ const human = humanCreator("Amu",23,{name:"Tatiana",parentName:"Amu"})
name: string;
parentName: string;
};
name: string;
age: number;
children: Kid | Kids;
};
return {
name,
age,
children
};
};
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.
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
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.
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:
- 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!