TypeScript zdobył oszałamiającą popularność w ostatnich latach, ale po co tak właściwie z niego korzystamy, jakie są jego plusy i minusy?
Zalety korzystania z TypeScript:
- wcześniej wychwytywane błędy
- przewidywalność działania dzięki statycznemu typowaniu
- czytelność kodu
- lepsze wsparcie edytora podczas developmentu
- łatwiejszy i szybszy refaktoring
- ograniczenie unit testów, przede wszystkim tych sprawdzających poprawność struktur
Wady stosowania kontroli typów:
- dodatkowy czas potrzebny na otypowanie kodu
- ograniczone zaufanie z uwagi na to, że koniec końców kod jest kompilowany do dynamicznego JS
- kompilator typu complete, częściej false negative (mimo błędnego kodu kompilacja zakończona sukcesem) niż false positive (brak błędu, jednak kompilacja failuje)
- narzut na czas kompilacji
TypeScript pozwala nam na sterowanie poziomem rygorystyczności. Jeżeli jesteśmy na etapie jego wdrażania w istniejącym już projekcie warto rozpocząć od od bardzo luźnej konfiguracji, natomiast startując z nowym projektem najlepiej skonfigurować go z poniższymi flagami:
- strict
- noUncheckedIndexAccess
Co w przypadku, gdy typ nie jest nam znany?
- typ any – można z nim zrobić wszystko, brak kontroli typu, użycie bardzo niewskazane
- typ unknown – nie można z nim zrobić nic, dopóki nie zastosujemy sprawdzenia rzeczywistego typu
Typy pomocnicze
Przykład kilku często bardzo ułatwiających typów pomocniczych dostępnych globalnie. Warto zapoznać się z całą listą dostępną w dokumentacji.
ReturnType
Otrzymujemy typ zwracany przez podany typ funkcji lub nazwę funkcji:
ReturnType<FunctionType>
type T = ReturnType<() => string>; // string
ReturnType<typeof functionName>
declare function fn(): { a: number; b: string };
type T = ReturnType<typeof fn>; // { a: number; b: string }
Partial
Otrzymujemy nowy typ którego wszystkie właściwości z typu źródłowego są opcjonalne:
Partial<T>
Partial<{name: string; point: number;}> // {name?: string; point?: number;}
Required
Typ wynikowy zawiera wszystkie właściwości z typu źródłowego jako wymagane:
Required<T>
Required<{name?: string; point?: number;}> // {name: string; point: number;}
Readonly
Otrzymujemy nowy typ którego wszystkie właściwości z typu źródłowego stają się readonly:
Readonly<T>
Readonly<{name: string;}> // {readonly name: string;}
Pick
Utworzony nowy typ zawiera jedynie właściwości wymienione jako drugi parametr:
Pick<T, Keys>
Pick<{name: string; point: number;}, "name"> // {name: string;}
Omit
Nowy typ powstaje z typu źródłowego z pominięciem właściwości wymienionych w drugim parametrze:
Omit<T, Keys>
Omit<{name: string; point: number;}, "name"> // {point: number;}
NonNullable
Tworzy nowy typ wyłączając z typu źródłowego null
i undefined
:
NonNullable<T>
NonNullable<string | number | undefined>; // string | number
Parameters
Wyciąga wszystkie parametry funkcji jako typ:
Parameters<T>
Parameters<(name: string, point: number) => void> // [name: string, point: number]
Dodatkowe ciekawostki:
Zwracanie this
Możliwe jest określenie zwracanego typu jako this
:
class ExampleClass {
method = (): this => {
return this;
}
}
const
assertions
Celowe zawężenie literałów:
let x = "hello" as const; // '"hello"'
let y = [10, 20] as const; // 'readonly [10, 20]'
let z = { text: "hello" } as const; '{ readonly text: "hello" }'
Unie i przecięcia
Unie i przecięcia to jedne ze sposobów łączenia typów:
- unia: A | B – typ wynikowy zawiera elementy obu zbiorów
- przecięcie: A & B – typ wynikowy zawiera elementy wspólne obu zbiorów
Unie dyskryminacyjne
Jeżeli pracujemy na typie, który składa się z unii podtypów, to warto aby każdy z podtypów zawierał właściwość określającą dany typ. W poniższym przykładzie jest to state, dzięki któremu pracując na typie NetworkState możemy zaimplementować różną logikę wykorzystując switch lub if.
type NetworkLoadingState = {
state: "loading";
};
type NetworkFailedState = {
state: "failed";
code: number;
};
type NetworkSuccessState = {
state: "success";
response: {
title: string;
duration: number;
summary: string;
};
};
type NetworkState =
| NetworkLoadingState
| NetworkFailedState
| NetworkSuccessState;
Dodatkowo implementując Union Exhaustiveness check możemy mieć pewność, że obsłużyliśmy każdy przypadek:
function logger(s: NetworkState): string {
switch (s.state) {
case "loading":
return "loading request";
case "failed":
return `failed with code ${s.code}`;
case "success":
return "got response";
default:
const _exhaustiveCheck: never = s;
return s;
}
}
W powyższym wypadku, gdyby typ NetworkState został w przyszłości rozszerzony o dodatkowy podtyp, TypeScript zwróciłby nam błąd informujący o tym, że nieobsłużony wariant który wpadnie w default nie jest typu never, więc nie może zostać przypisany do stałej _exhaustiveCheck.
Typeguards
Sposoby na sprawdzenie typu obiektu:
typeof
if (typeof arg === 'string') {
// Within the block TypeScript knows that value must be a string
}
instanceof
if (arg instanceof ClassName) {
// Within the block TypeScript knows that value must be an instance of ClassName
}
in
if (prop in object) {
// Within the block TypeScript knows that property exists on an object
}
Literal type guard
if (s.state === "success") {
// Within the block TypeScript can discriminate between union member
}
Custom type guard
function isClassName(arg: any): arg is ClassName {
return (typeof arg.name === 'string');
}
Weak types
W przypadku, gdy interfejs posiada wszystkie pola typu optional jest postrzegany jako weak type. Jeżeli spróbujemy przypisać do takiego typu dane, które nie mają ani jednej wspólnej właściwości, to TypeScript zwróci błąd.
Generic constraints
Mamy możliwość napisania funkcji generycznej, której typy będą przez nas w pewien sposób ograniczone.
function merge<U extends {length: number;}, V extends object>(obj1: U, obj2: V)
function prop<T, K extends keyof T>(obj: T, key: K)
Conditional Types
Możemy wykorzystać również ternary operator znany z JavaScriptu, pozwalający za pomocą warunku stworzyć konretny typ.
SomeType extends OtherType ? TrueType : FalseType;
type Example = Dog extends Animal ? number : string;
infer keyword
Słowo kluczowe infer
pozwala na wywnioskowanie typu z innego typu w ramach typu warunkowego.
type Foo<T> = T extends { a: infer U; b: infer U; } ? U : never;
Foo<{ a: string; b: string }>; // string
Foo<{ a: string; b: number }>; // string | number
type FlattenIfArray<T> = T extends (infer R)[] ? R : T
FlattenIfArray<string[]>; // string
FlattenIfArray<number>; // number
Zakończenie
Jeżeli chciałbyś dodać coś od siebie, zachęcam do dyskusji w komentarzach 🙂