- po co type checker?
- redukujemy naszą odpowiedzaloność za sprawdzanie typów
- zaufanie do kompilatora, mamy większy komfort pracy
- wychwytywanie błedów wcześniej, w compile-time zamiast runtime
- mniej debugowania!
- nie potrzebujemy testów dla poprawności struktur, type checker to już robi za nas
- typ to podpowiedź, szczególnie dla innych programistów którzy używają nas kod
- pozwala zapisać intencje programisty
- ogranicznia TypeScripty
- nie sprawdziwmy poprawności odpowiedzi z API
- to jest nieznane do momentu odpalenia kodu
- warunki wyścigu (race conditions)
- problem kiedy TypeScript nie jest pewien czy zmienna jest już zainciowana
- tutaj możemy to potwierdzić że tak będzie poprzez znak ! ale bierzemy na siebie odpowiedzalność
- niektóre operacje JS koercja, TypeScript pozwala na to co już silnie wrosło w JS-owa tradycje
- {} + ''
- 100 / 0 -> to przejdzie chociaż i tak dostaniemy wynik w postaci inifinity
- nie sprawdziwmy poprawności odpowiedzi z API
- kompilator powiada nam gdzie kod może się wywalić
- znajdujemy błędy we wczesnym etapie
- nie powinniśmy czekać z upgradewaniem wersji TS, czym później tym to będzie trudniejsze
- to jak z pull requestami, czym dłużej leża tym merge będzie trudniejszy
- TS możemy wypróbować online -> https://www.typescriptlang.org/play
- Też się sprawdza do testowania nowej wersji
- Nowe wersje wychodzą średnio co 2 miesiące :o
- jest to przydatne w przypadku dzielenie się kodem
- my sami znamy nasz kod, inny nie koniecznie
- jeśli jest jaka korzyść, to musi być cena
- tutaj ceną jest potrzeba znajmości typów
- popularne type checkery w świecie JS
- TypeScript
- Flow
- PureScript
- Hegel
- Najlepszym wyborem jest TypeScript ponieważ jest najbardziej popularny
-
typowanie statyczne vs dynaminczne
- typowanie statyczne wymaga od nas określenai jaki to będzie typ danych
- spowolni pisanie kodu bo musimy przmyśleć jakie typ danych to będzeie
- jakość kodu wzrośnie, jest to inwestycja aby mieć mniej bugów w kodzie
- typowanie dynamiczne nie wymaga myślenia o typie, jest to płynne
- prostsze w użyciu, szybsze
- ceną jest mniejsza jakość kodu, więcej potencjalnych runtime errorów
- mimo szybszego developementu, poprawa błedów może skutować utratą czasu
- typowanie statyczne wymaga od nas określenai jaki to będzie typ danych
-
typowanie silne vs słabe
- ocena nie jest 0/1, różne jezyki mają różny poziom silności/słabości, to często ocena subiektywna, zależna od programisty
- dla nie poprawnych operacji
- silne rzuci błedem
- wolimy to w bardziej "poważnym" sofcie
- operacja 5 / 0 nie pozwoli na kompilacje kodu
- tzw. loud fail
- słabe probram działa dalej "jakoś"
- tzw. silent fail
- np. dla operacji 5 / 0 -> dostaniemy infinity zamiast bład działania
- jest to dobre dla poczatkujących
- każdy słaby język ma inną graince akceptowalności błedów
- w ES6 js nie akceptuje dwóch letów dla zmiennej
- silne rzuci błedem
-
przykłady znanych jezyków w kwestii bycie statycznym i silnie typowanym
- java -> statyczna & silna
- python -> dynamiczny & silny
- js -> dynamiczny & słaby
- TS -> stopniowo typowany - stopień silności zależy od ustawień
-
poliformizm w TS
- apparent type - jak kompilator to widzi, co wie
- actual type - czym realnie jest obiekt (RunTime)
-
Typowanie: strukturalne vs nominalne
- Pytanie, co to znaczy że coś jest kompatibilne z intefejsem X?
- Nominalne - implementuje Java, C#
- istotna jest nazwa klasy/intefejsu, hierarchia dziedziczenia
- dwie klasy o tej samej strukturze NIE SA TOZSAME
- poliformizm oparty o klasę bazową interejs
- Strukturalne - implementuje TS
- istotnaj jest zawartość obiektu
- dwie identyczne struktury są TOŻSAME
- poliforizm strukturalny
-
Anotacje vs asercje (Typów)
- wnioskowanie typu
- var e = value -> typ e przyjmie domyślnie typ value (kompilator domyśli się)
- anotacja typu
- var e: string = value -> typ e to string! Mamy Type Safe!
- asercja typu
- var e = value as string -> Wiemy lepiej co to jest, wymuszamy typ!
- jest to niebezpieczne, możemy się pomylić
- tracimy korzyści z Type Safe
- asercja istnieje jako "furtka" w momencie jak kompilator źle wnioskuje typ danych
- stosujemy to jako ostateczność!
- wnioskowanie typu
-
Technniczne TypeScript posiada dwie przestrzenie nazw (namespace)
- przestrzeń nazw
- to przenika do JS
- przestrzeń typów
- to istnieje tylko w TS, ulotni się podczas kompilacji
- technicznie rzecz biorąc możemy mieć zmienną oraz typ danych o tych samych nazwach, to możliwe!
- przestrzeń nazw
-
Wnioskowanie Typów
- Różnicą pomiędzy instrukcją a wyrażeniem
- instrukcja to najmniejsza jednostka, jakiś rozkaz, bez typu
- wyrażenie to rzeczownik, jakaś rzecz, mają typ
- wyrażenie można przypisać do zmiennej, instrukcji nie
- Różnica let czy const
- let powoduje że zmienna na typ, może sie zmienić
- const powoduje że to jest jakaś wartość, jeśli nie ustalimy typu to przyjmie to przypisaną wartość jako stałą
- const x = 'napis' => typ -> "napis"
- const y: string = 'napis' => typ -> string
- Różnicą pomiędzy instrukcją a wyrażeniem
-
Zbiory zmiennych
-
Typy to zbiory
-
Typy top & bottom
-
top - czyli wszystko
-
możemy do nich przypisać dowolny elemement
-
any - możemy stosować wszędzie
- niebezpieczny, type unsafe, sprawaia że kompilator zamyka oczy
- gubi błedy, bo wszystko jest zgodne w obie strony
- jeżeli nadużywamy any to po co stosować TS?
- ma to zastosowanie w fomie "poddania" się kiedy np. walczymy z zewnętrzną biblioteką
- typy zbliżone
- Function - any wśród funkcji
- Object - prototyp wszystkich funkcji w js
- object - non-primitive type
-
unknown - nie możemy z tym nic zrobić do póki nie sprawdzimy czym jest, nic nie jest gwarantowane
- czyli w funkcji musimy zweryfikować typ aby potwierdzić czym jest
- to nam daje bezpieczeństwo w kodzie, należy zweryfikować czym jest zmienna
- stostujemy wtedy kiedy nie wiemy czym coś jest
type Gruszka = { kolor: string }; type GruszkaSoczysta = Gruszka & { kolor: string } // customowy type guard, aby sprawdzić czy obiekt jest kompatybilny! function customTypeGuardGruszka(a: any): a is Gruszka { return (a as Gruszka).kolor !== undefined; } function zjedzGruszke(gruszka: unknown): Gruszka { if (customTypeGuardGruszka(gruszka)) { return gruszka; } return { kolor: 'asds' } }
-
-
bottom - czyli nic, zbiór pusty
- never
- stosuje się dosyć rzadko, aby rzucić wyjątkiem
- zwracają ja funkcje które są zapętlone
- TS zwraca taki typ gdy przecięcie zbiorów typów jest puste
- systemów typów potrzebuje mieć sufit oraz podłogę tak aby mieć od czego się odbić
- never
-
-
-
string, number, boolean to osobne zbiory
-
Unie i przecięcia
-
znakiem | ozbaczamy unie
- w tym przypadku TS nie wie ktorym typem jest obiekt, to powoduje że będzie wyrzucał błąd przy próbie wywołania metody jednego z nich
- będziemy mieć dostępne tylko to co występuje w obu obiektach jednocześnie np. pole name
-
znakiem & przecięcia
- to tak jakby klasa implementowałą dwa interfejsy
- będziemy mieć dostępne wszystkie wspólne pola (to musi być jednoczęsnie połączenie obiektów)
-
unie dyskriminacyjne
- wykorzystujemy wspólne pole Type aby podpowiedzieć jaki to typ
type A = { type: "A", uniqueField: "x" }; type B = { type: "B" }; type C = { type: "C" }; type Union = A | B | C; type PropType = Union["type"]; function someFunction(someParam: Union) { switch (someParam.type) { case "A": // tzw. type guard someParam.uniqueField; // TS już wie że to będzie typ A! break; default: // tzw. exhaustiveness type let x: never = someParam; // feature w TS, zabezpieczenie przed tym aby nie zapomnieć o dodaniu nowego typu } }
-
-
opcja --strictNullChecks=false pozwala na przypisanie nulla do string
-
Typy vs Intefejsy
- obiekty można otypować typem oraz intefejsem
- typy i interfejsy można rozszerzać, dziedziczyć oraz implementować
- więkoszości przypadków możemy używać ich zamiennie, nie ma to takiej różnicy
- jakie są różnice?
- declaration merging - tylko intefejsy mogą być mergowane do jednego wspólnego jeśli występują w wielu miejsach w kodzie (redux i redux-thunk)
- interejsy muszą znać wszystkie pola, odpadają unie i typy warunkowe
- Twórcy TS zalecają stosownie Intefejsów, może to przyspieczać kompilowanie kodu
type FajnyType = { // to zadziała name: string } | { value: number } interface FajnyInterfejs { // nie zadziała, ale za to jest szybsze w kompilacji name: string } | { value: number }
// to zdzaiała! TS łaczy intefejsy w trakcie kompilacji! interface Podstawowy { name: string; } interface Podstawowy { value: number; }
-
-
Kontrola przepływu w aplikacji (Control flow analysis)
- Używamy type checkerów aby wyłapywać błedy w kompilacji zamiast w runtime
- kompilator analizuje możliwy przepływ w kodzie
- zawęża typy do gwarantowanego typu (np. if)
declare const a3: true | 0 | 'a' | undefined; declare const b3: false | 1 | null; const c3 = a3 || b3; // TS wyliczy możliwe wartości w przypadku takiej operacji const c4 = a3 && b3; if (a3) { // przechodzą tylko truthy console.log(a3); }
- Potrzebujemy Type Guard aby w runtime potwiedzić czym jest zmienna
// type guard if (typeof a3 === 'number'){ console.log(a3) } // bardziej rozbudowane type guard interface ŁosośNorweski { smaczny: boolean krajPochodzenia: string } // czyżbyŁosoś is ŁosośNorweski -> ważny zapis, to podpowiada TS do czego sluży funkcja, aby potwierdzić czym jest zmienna function jestŁososiemNorweskim(ryba: any): ryba is ŁosośNorweski { return ( typeof ryba.smaczny == 'boolean' && ["Brazylia", "Wietnam", "Chile"].includes(ryba.krajPochodzenia) ) } declare const rybaZLeklerka: unknown if (jestŁososiemNorweskim(rybaZLeklerka)){ console.log(rybaZLeklerka) }
- Assert functions - specjalny typ funkcji który pozwala w kodzie wykonać asercje że dany obiekt jest tym co powinnien
function awanturaJeśliNieŁosoś(czyżbyŁosoś: unknown): asserts czyżbyŁosoś is ŁosośNorweski { if (jestŁososiemNorweskim(czyżbyŁosoś)){ // jeśli pochodzi z Chile to jeszcze przejdzie // ale Brazylia i Wietnam to już nie if (czyżbyŁosoś.krajPochodzenia !== 'Chile') { throw new Error('Żądam zwrotu pieniędzy') } } else { throw new Error('Żądam zwrotu pieniędzy') } } // wywołanie function obiadWWykwintnejRestauracji(){ rybaZLeklerka // unknown awanturaJeśliNieŁosoś(rybaZLeklerka) rybaZLeklerka // w tym momencie to jest potwierdzenie że to łosoś! WOW! }
- W przypadku przypisania do zmiennej any konkretnego typu, zmienna przejumuje ten konkretny typ!
declare const naPewnoŁosoś: ŁosośNorweski let cokolwiek // TS daje typ any, nie wie co to ma być cokolwiek = naPewnoŁosoś cokolwiek // typ to już ŁosośNorweski
- Jeśli w kodzie mamy zmienną która może być undefined to najlepiej zaraz po jej wystąpieniu dodać warnek z exception. Dzięki temu TS już wie że musi być tym oczkiwanem typem. Ścieżka undefined jest bardzo krótka i kończy się na exception
let musiBycLosos: ŁosośNorweski | undefined; if (!musiBycLosos) { throw new Error("TO NIE LOSOS"); } console.log(musiBycLosos);
-
Generyki
- Typ generyczny - to typ który parametryzuje inne typy
- przyjmując typ tworzy nowy typ, podobnie jak funkcja zwraca dla prametru
const combine = (a, b) => ({a, b}) // bez generyka const combine<A, B> = { a: A, b: B } // generyk
- przykład generyka
// typ generyczny T jest WYMAGANY type Storage<T> = { data: T[] add(t: T): void } interface IStorage<T> { data: T[] add(t: T): void }
- Generic contraint (obostrzenia) - sposób aby wymusić jakaś strukture/typ generycznego typu, np. aby miał pole id z numericiem
class AnotherStorage<T extends { id: string }> { // w tym przypadku extends oznacza że typ powinnien rozszerzać podany typ danych! constructor( public data: T[] ){} add(t: T){ this.data.push(t) } findById(id: string){ return this.data.find(item => item.id == id) // ❌ // nic nie gwarantuje, że `id` istnieje } } const anotherStorage = new AnotherStorage([{id: 'ANF'}]) const element = anotherStorage.findById('95c5a122-6973-4139-98ea-7e23f3ea3546')
- Generyczne funkcje przykłady
function combineFn<T>(a: T, b: T) { return { a, b }; } // return type: { a: T, b: T } // generyk może być INNY dla każdego WYWOŁANIA // (nie jest stały dla funkcji) const combinedStrings = combineFn("a", "b"); // { a: string, b: string } - automatycznie TS dopasowuje typ generyczne po parametrach const combinedNumbers = combineFn < numeric > (1, 2); // { a: number, b: number } - nadal możemy jawnie wskazać jaki to typ danych
-
Generyczne funkcje vs funkcja ze sparametryzowanym typem
- funkcja ze sparametryzowanym typem jest inna, poniważ już na starcie ustalamny dla jakiego typu będzie działać, nie można tego zmienić później
type GenericFn = <T>(a: T, b: T) => { a: T, b: T } type ParametrizedFn<T> = (a: T, b: T) => { a: T, b: T } declare let _parametrizedFn: ParametrizedFn // ❌ musi mieć z góry znane T declare let parametrizedFn: ParametrizedFn<string> declare let genericFn: GenericFn // nie musi, bo każde wywołanie może mieć inne T parametrizedFn('ANF', 'ANF') // ✅ miał być string parametrizedFn(125, 125) // ❌ miał być string genericFn('ANF', 'ANF') // ✅ cokolwiek genericFn(125, 125) // ✅ cokolwiek
-
Generyk na poziomie klasy
// jeden wspólny generyk na poziomie klasy class GenerykKlasy<T> { constructor( public data: T ){} metoda1(another: T){ return this.data == another } metoda2(another: T){ return this.data === another } } const obiektA = new GenerykKlasy('ANF') obiektA.metoda1('ANF') obiektA.metoda1(125) // ❌ zgodnie z oczekiwaniami obiektA.metoda1(true) // ❌ zgodnie z oczekiwaniami
-
Typy mapowane
- typ wtórny - typ na postawie innego typu
- iterowanie po kluczach interfejsu
- mapowanie typów analogicznie do mapowania danych
export interface Transfer { id: string; amount: number; title: string; payerAccount: string; beneficiaryAccount: string; beneficiaryAddress?: string; scheduledAt: string; } type T1 = Partial<Transfer> // Partial - sprawia że wszystkie pola będą opcjonalne type T2 = Required<Transfer> // Required - sprawia że wszystkie pola będą wymagane // type T2 = Required<Partial<Transfer>> type T3 = Pick<Transfer, "id" | "amount"> // Pick - tylko zawiera wymienione pola type T4 = Omit<Transfer, "id" | "amount"> // Omit - odwortnie, wszystko oprócz tych wymienionych pól type Reveal<T> = { [P in keyof T]: T[P] } type RequiredFields<T, K extends keyof T> = Reveal< Required<Pick<T, K>> & Omit<T, K> > // Reveal - upraszcza typ do tego czym realnie będzie :o type X = RequiredFields<{ a?: number, b?: number }, 'a'>
-
Typy warunkowe
- Można porównać do konstrukcji if/else
type X = T1 extends T2 ? A : B
-
Warunek odpowiada na pytanie czy T1 jest podzbiorem typu T2
-
distributive/naked - rodzielność IF-owania (tylko dla typów naked)
- naked czyli typ nie jest opakowany w np. array
- co istotne aby typ był rozłączony musi być naked
// naked: T extends string type OnlyStrings<T> = T extends string ? T : never type OnlyStringsSkills = OnlyStrings<Skills> // not-naked: T[] extends string[] type _OnlyStrings<T> = T[] extends string[] ? T : never type _OnlyStringsSkills = _OnlyStrings<Skills>
- naked czyli typ nie jest opakowany w np. array
-
infer - tylko dostępne w warunkach - pozwala wydobyć typ z większego typu
// jak wydobyć typ parametru obiektu? type Person = { name: string; age: number } type PersonProperty = Person['name'] // a parametr z funkcji? type PorytaFunkcja = (arg1: { value: number }, arg2: { date: Date }) => { value: number, date: Date } type FirstArg<T> = T extends (arg: infer A, ...args: any[]) => any ? A : never // wyciągamy typ pierwszego parametru funkcji czyli arg1 type FirstArgOfPorytaFunkcja = FirstArg<PorytaFunkcja> // albo typ wynikowy funkcji? // analogicznie - ale odpuszczamy pisanie ręczne, bo są wbudowane: type T1 = Parameters<PorytaFunkcja> type T2 = ReturnType<PorytaFunkcja> // często wykorzystywane, warto zapamietać // a dla funkcji (a nie typu funkcyjnego) trzeba dodatkowo typeof const poryta = (arg1: { value: number }, arg2: { date: Date }) => ({ ...arg1, ...arg2 }) // type T3 = ReturnType<poryta> // ❌ namespace mismatch type T3 = ReturnType<typeof poryta> // istotne musimy użyć typeof poryta aby uderzyć do namespace typów
-
rozłaczność (distributive)
- rozłączność polega na tym że warunek zostanie zaplikowany dla każdego elementu unii
type NonNullableSkills = NonNullable<Skills>; // tylko pola które nie są nullowe tzw. distributive (rozłączne)
- Typ generyczny - to typ który parametryzuje inne typy
-
Type-unsafe (dziury w kompilatorze)
-
Mimo wszystko TS posiada dziury i nie zweryfikuje wszystkiego
-
Jednym z powodów jest koszt obliczeniowy kompilacji, natomaiast jednym z głównych założeń TS jest szybkość kompilacji
-
array access np. arr[10]
// 1. elementA jest oczywiście numberem const array = [1, 2, 3, 4, 5]; const elementA = array[0]; // 2. ale to nie powinno być const elementB = array[5]; const elementC = array[10]; // to nie istnieje ale nadal mamy number..
-
index signature
export {} type Value = number type ItemMap = { [key: string]: Value } declare const map: ItemMap type ItemRecord = Record<string, Value> declare const record: ItemRecord // 1. intro // 2. dodanie mapItem i recordItem poniżej // 3. najpierw było string -> number, a teraz zamieniamy number -> string, dalej unsafe const mapItem = map[1] const mapItem2 = map['elo'] // to nie istnieje, nadal mamy że zwróci number.. const recordItem = record[1] const recordItem2 = record['elo']
- flag kompilacji "strict"
- Domyślnie TS nie wszystko sprawdza, tak aby ułatwić migracje z czystego javascript
- opcja "strict" uruchamia wszytskie możliwe opcje (hurtowo)
- to dobry pomysł aby na samym początku opcja była uruchomiona
- wyłączenie opcji strict pomaga wykonać migracje większego kodu js to TS
- tutaj ma to najwięcej sensu, dla projektów legacy
- NoImplicityAny
- W przpypadku kiedy zmienna domyślnie jest any, to TS wyrzuci bład, tak abyśmy musieli świadomienie otypować to any (wiemy co robimy)
- NoImplicityReturn
- Każda ze ścieżek funkcji musi zwrócić wartość
- Uderzy błedem jeśli jest możliwość że funkcje nie wykona return
- strictPropertyInitalization
- Jeśli tworzymy jakieś pole na klasie to musi zostać zainicowane
- strictNullChecks
- Wymusza abyśmy jasno mówili że zmienna będzie zawierać null / undefined
- noUncheckedIndexAccess ("linter check")
- zmienia logikę typu zwracanego w ramach odczytu indeksu array'a
- od teraz zawsze zwraca "typ | undefined", co wymusza na nas sprawdzenie czy dany indeks realnie istnieje!
- Uwaga! Nie jest w ramach "strict" należy dodać ręcznie do konfiguracji
- zmienia logikę typu zwracanego w ramach odczytu indeksu array'a
- strictFunctionTypes
- Wyłaczona powoduje że parametry funkcji są sprawdzane przez biwariancje
- można przekazać typ, podtyp oraz nadtyp
- Wlączona powoduje że parametry funkcji są sprawdzane przez kontrawariancje
- można przekazać typ oraz nadtyp
- Uwga działa to tylko na arrow function, dla zwykłych metod mamy nadal biwarancyjne podejście
- Jeśli chcemy z tego korzytać to np. na interfejsie powinniśmy definiować metody za pomocą array function np. () => void
- Wyłaczona powoduje że parametry funkcji są sprawdzane przez biwariancje
- False positive i False negative
- False positive - złodzieja nie ma, jest alarm (type | error)
- False negative - jest złodziej, nie ma alarmu (type & error)
- W praktyce nie chcielibyśmy aby występowało powyżej, ale to nie realne
- Mamy trzy faktory sound, complete, roztrzygalny
- Możemy spełnić tylko dwa, rozstrzygalne jest potrzebny
- Mam do wyboru być sound lub complete
- TS wybrał complete, dlatego potrawi przepuśić kawałki kodu które się kompilują ale walną błedem runtime'owym
-
-
Wzorce i antywzorce
-
Single source of truth
- problem
- konieczność jednej logicznej zmiany w wielu miejscach wskutek kopiowania typów
- cel
- type flow - zmiana źródłej deklaracji typu aktualizuje miejsca użycia
- rozwiązanie
- centralna deklaracja typu -> type Money = number
- problem
-
Primitive Obsession
-
zamiast stworzyć osobny obiekt używamy prymitywów, co powoduje
- możemy zrobić z nimi wszystko, nawet jeśli nie ma to sensu w logice biznesowej
- dodanie dodatkowej informacji jest problematyczne, np waluty do kwoty. Musimy dodawać kolejne zmienne..
-
problem
- nadużywanie typów prymitywnych
-
cel
- uniemożliwinie operacji niedozowlonych
- ogranicznie kompatibilności
-
rozwiązanie
-
Oparque/Brand types
export {} type Money = number & { readonly type: unique symbol } // unique symbol wymsza niekompatibilność jeśli chodzi o pole type (cały typ Money). To taki trik! declare let m: Money declare let n: number m = n // ❌ Type 'number' is not assignable to type 'Money'. n = m // ✅
- tylko deklaracja typu
- blokowanie kompatibilności pomiędzy typami
- plusem, minimalny narzut
- minusem, dyscyplina
-
Value Object
export {} type Currency = "EUR" | "PLN" class Money { private constructor( private value: number, private currency: Currency, ){} // prywatny konstruktor & statyczna metoda fabrykująca static from(value: number, currency: Currency){ return new Money(value, currency) } // możemy mnożyć tylko przez współczynnik (liczbę) multiply(factor: number){ return new Money(this.value * factor, this.currency) } // chronimy reguł biznesowych: // można dodawać tylko pieniądze w tej samej walucie add(another: Money){ if (this.currency != another.currency){ throw new Error('Cannot add different currencies') } return new Money(this.value + another.value, this.currency) } nominalValue(){ return this.value } } const m = Money.from(99.99, "PLN") // deklaracja m + 4 // ❌ Operator '+' cannot be applied to types 'Money' and 'number'. const n: number = m // ❌ is not assignable to type 'number' const sum1 = m.add( Money.from(1.23, 'PLN') ) // ✅ Money const sum2 = m.add( Money.from(1.23, 'EUR') ) // ✅ kompiluje się (bo typy są zgodne) // - ale wybuchnie w runtime const product = m.multiply( 2 ) // ✅ Money
- klasa -> typ oraz implementacja
- blokowanie kompatibilności pomiędzy typami
- wyszczególnienie poprawnych operacji (i nie poprawnych)
- plus, łatwiejsze w zrozumieniu i utrzymaniu
- minus, implementacja i wywołanie oraz gorsza wydajność w runtime
-
Boolean Obsession
- problem
- nadużywanie booleanow tam, gdzie nie sa wystarczajace
- stan z wieloma polami false/undefined
- odpowiedzalność za manulaną obsługę komórek stanu
- cel
- uniemożliwić tworzenie stanów niepoprawnych
- wysokopoziomowa obsługa stanu jest łatwiejsza w utrzymaniu
- rozwiązanie
- użycie bardziej precyzyjnych typów np. unii
- problem
-
-
Axios jest lepszy od fetch bo można zdefiniować do dostaniemy w zwrotce
export const _getTransfers = () => { return axios.get<Transfer[]>('/account/transfers') .then(res => res.data) .then(collection => collection.mkjhgbvnmjhgvbnmjhgv) } // uf 😅
-
-
DTO
- obiekty do transferu danych
- zero logiki biznesowej
-
-
TypeScript - zyski i straty
- plusy
- wychwytywanie błędów wcześniej (compile-time)
- mniej unit-testów, kompilator pilnuje wiele ścieżek (flow)
- łatwiejsze rozumienie kodu i intencji programistów
- stabilniejsza praca zespołowa
- zmieniając coś w typie kompilator pokaże gdzie będą problemy (brak kompatibilności)
- wychwytywanie błędów wcześniej (compile-time)
- minusy
- dodatkowy krok: kompilacja
- wnosi swoje complexity
- złudne poczucie pełnego bezpieczeństwa typów
- plusy
W Ts mamy specjalny wyjątek gdzie nie możemy przypisać rozszerzonego literału do typu obiektowego, ma to taki cel aby wyłapywać literówki w przekazywanych parametrach do funkcji
Pozwala to wypłapać takie przypadki:
type Konfiguracja = { version: "4" | "5" };
function MojaBiblioteka(conf: Konfiguracja) {}
MojaBiblioteka({ version1: "4" }); // błąd, literówka w literale
Jakby nie było takiego wyjątku to moglibyśmy mieć nie fajne bugi, tzw. silent fail
zespół TS chciał aby takie przypadki to było zawsze loud fail
Weak type w TS to obiekt który posiada wszystkie opcjonalne pola.
Jest weak ponieważ można do niego przypisać dowolny inny obiekt.
Tutaj mam wyjątek, TS wali błedem jeśli chcemy przypisać do weak type obiekt który nie posiada chociaż jednego wspólnego pola.
Celem jest wyłapanie prawdopodonych czeskich błędów.
przykład
type myWeakObject = {
name?: string,
value?: string,
};
const instanceOfMyWeakObjecy = { someNotExisingField: "asds" };
function weakFunction(o: myWeakObject): void {}
weakFunction(instanceOfMyWeakObjecy); // bład! brak ani jednego wspólnego pola z myWeakObject
W TS nie powinniśmy stosować typu Object, to element samego JS i pozwala na takie cos jak
let y1: Object = 4; // zadziała :o
to wynika z tego że w JS mamy autoboxing i konwertuje 4 do obiektu
zmiast tego stosujemy typ zmałej litery (TS) czyli object
let y2: object = 4; // nie zadziała, oczekuje realnego obiektu
PropertyKey to specjalny typ w TS, pasuje idealnie do klucza obiektu (generycznego) = string | number | symbol
Znak ! przy zmiennej/wywołaniu funkcji oznacza że deklarujemy że dana wartość nie będzie nullem. TS w takim przypadku "ufa" nam.
let someObject: {key: string};
function someFunctionReturningSomeObject(): someObject | null; // przykładowa funkcja, funkcja zwrawca someObject | null
let concreteSomeObject = someFunctionReturningSomeObject()!; // Uwaga! Znak ! na końcu stwierdza że mamy pewność że nie będzie to null!
concreteSomeObject.key = 'someString'; // jeśli byśmy nie użyli ! w poprzedniej linijce, byśmy mieli problem z kompilacją
Ewentualną aleternatywą jest type guard
let someObject: {key: string};
function someFunctionReturningSomeObject(): someObject | null; // przykładowa funkcja, funkcja zwrawca someObject | null
let concreteSomeObject = someFunctionReturningSomeObject(); // Uwaga! Znak ! na końcu stwierdza że mamy pewność że nie będzie to null!
if (concreteSomeObject) { // type guard, gwarantuje że nie będzie to null <3
concreteSomeObject.key = 'someString'; // jeśli byśmy nie użyli ! w poprzedniej linijce, byśmy mieli problem z kompilacją
}
Możemy oznaczyć że parametr nie będzie używany, mimo że jest zdefiniowany. Tak aby TS nie zgłaszał tego jako błąd
function someFunction(_: string) {
// brak błedu, mimo że nie używamy parametru
return null;
}
- ESLint - sprawdzanie jakości kodu
- Prettier - CodeFormatter
- Debugger for Chrome
- Wymaga opcji "sourceMap" na true
- Jest to nakładka na JS
- wprowadza silne typowanie do JS
- kompiluje do natywnego JS'a
- W przypadku braku jawnego typowania ts domyśla się typu po przypisanej inicacyjnej wartości
- wszystkie typy w TS sa zapisane małymi literami np. string, number
- @ts-ignore
- ignorowanie konretnej linii kodu
- @ts-expect-error
- podobne do powyższego ale podczas kompilacji wyrzuca informacje jeśli w tym miejscu nie mamy błedu
- @ts-nocheck
- ignorowanie całego pliku (dodajemy u góry pliku)
Powyzsza adnotacje dodajemy jako komentarz
// @ts-ignore
let ts: string = 3;
let myVariable: string;
class SomeClass {
name: string;
describe(this: SomeClass) {
// dzięki takiemu zapisowi mamy gwarancje że będzie to wywołane tylko z obiektu SomeClass
return this.name;
}
}
Jest to funkcjonalność TS (nie istnieje w JS). Umożliwa oznaczeni pól klasy jako tylko do odczytu.
class SomeClass {
constructor(private readonly id) {
}
}
protected, public to dodatek TS. Nie istnieje odpowiednik w vanilla js
Możemy wykorzystać specjalny zapis js dla getterów i setterów
class SomeClass {
private someParam: string;
// słowo kluczowe get
get getSomeParam() { // nazwa nie może być taka sama jak nazwa parametru
return this.someParam;
}
set setSomeParam(value: string) {
this.someParam = value;
}
}
// następnie używamy pól jak atrybutów
someClassObject.getSomeParam; // zwara wartość gettera
someClassObject.setSomeParam = 'someNewValue'; // wywołuje settera klasy
- instalacja poprzez npm, instalujemy w trybie globalnym
npm install -g typescript
- Tworzymy plik TS np. nazwa_pliku_do_kompilacji.ts
let somets: string = "test";
- Następnie używamy typescript poprzez polecenie tsc
- W parametrze podajemy plik TS do kompilacji
tsc nazwa_pliku_do_kompilacji.ts
Umożliwia uruchomienie tryby w którym plik zostanie przekompilowany w momencie jak zajdzie jakaś zmiana
tsc --watch twoj_plik.ts
Aby kompilować wszystkie pliki w projekcie wykonujemy polecenie
tsc --init
to nam tworzy plik tsconfig.json w projekcie, nic mie musimy z tym robić. Mamy tam konfiguracje TS (jeśli chcemy to zmieniamy)
Następnie odaplamy polecenie
tsc;
aby przekompilować cały projekt lub to samo w trybie --watch
Aby excludować plik z kompilacji, w pliku tsconfig.js
...
"exclude": {
"analytics.ts"
"node_modules" // warto dodać aby tego nie kompilować
// *.dev.ts - aby excludować wszystkie takie pliki
// **/*.dev.ts - aby excludować wszystkie takie pliki w dowolnym katalogu
}
aby includować pliki do kompilacji, w pliku tsconfg.js
"include": {
"app.ts",
...
}
pominięte pliki w include zostana zignorowane, wiec to przydatne gdy nie chcemy robić dużej listy w exclude
target określa do jakiej wersji JS chcemy kompilować nasz kod TS'a. To ma znaczenia obsługi przez przeglądarki.
TS umożliwa że kompiluje es5, który nie posaida let oraz const.
umożliwa określenie jakie opcje posiada TS. Na przykład obsługę globalnej zmiennej document. Domyślnie posiada opcje aby to złapać.
Umożliwa aby TS akceptował zwykły JS w samym sobie. Przydatne kiedy mamy jakies legacy i nie chcemy tego przepisywać.
TS będzie też sprawdzał pliki .js zamias tylko .ts
Jak ustawimy na true to generuje nam plik .js.map plik, to powoduje że przegladarka widzi wszystkie pliki projektu. (do przetestowania)
Jest to przydatna opcja do debugowania, ponieważ możemy debugować nasz kod JS w samej przegladarce.
umożliwa zmiane struktury projektu, na przykład jak chcemy aby TS generował pliki js w katalogu dist itp.
Usuwanie kometarzy w finalnym buildzie TS
Umożliwa zablokowanie generowania pliku TS jeśli znajdują się w nim błędy. Przydatne, domyślnie sa generowanie pliki co może prowadzić do "olewania" problemów.
oznacza że wszystkie opcje sprawdzania kodu są uruchomoione.
Natomiast mamy konkretne opcje
- NoImplicitAny - blokuje używania parametrów które nie sa jasno określone. Nie akceptuje typu "any"
- strictNullChecks - blokuje zmiennej które potencjalnie mogą być nullem (brak inicjacji). Na przykłąd pochodzi ze funkcji która MOŻE zwrócić null'a
- strictFunctionTypes - sprawdzanie syngnatury funkcji
- strictBindCallAplly - sprawdzanie czy przekazujemy wszystkie potrzebne parametry
-
number
- 1, 5.3, -10
-
boolean
- true, false
-
string
- 'Hi', "Hi",
Hi
- 'Hi', "Hi",
-
object
-
w przypadku braku typowania, TS ustawi domyślne typy po inicjujacej wartości kluczy
-
domyślnie typujemy poprzez "object", natomiast to powoduje że TS nie ma informacji o typach pól, powoduje to potem problemy przy kompilacji
```js const person: object = { name: "test" }; console.log(person.name); // bład kompilacji, to tylko pusty obiekt dla TS ```
-
jeśli chcemy określić jak powinnien być zbudowany obiekt zapisujem to jak poniżej
```js const person: { name: string } = { name: "test" }; console.log(person.name); // brak błedu kompilacji! TS wie czego się spodziewać ```
-
w przypadku zagnieżdzania obiektu zapisujemy to jak poniżej
```js const person: { id: string, price: number, tags: string[], details: { title: string, description: string, }, } = { id: "abc1", price: 12.99, tags: ["great-offer", "hot-and-new"], details: { title: "Red Carpet", description: "A great carpet - almost brand-new!", }, }; ```
-
-
array
- może przechowywać dowolną kolelcje typów np. number, string itp.
- musimy określić typ danych w array np. string[] lub określić że typy moga być dowolne (mieszane) poprzez any[]
-
tuple
- Jest to array z określonymi typami elementów
- np. [number, string]
-
enum
-
Typ dodany przez TS
enum Role { ADMIN, READ_ONLY, USER }
-
TS pod spodem zamienia to na integer, ale zystkujemy możliwość czystego kodu
-
ewentualnie możesz ustawić wartość enum
enum Role { ADMIN = 'ADMIN', READ_ONLY = 'READ_ONLY', USER = 'USER' }
-
-
any
- Dowolny typ, wylaczenie komplilatora TS
- nie jest zalecane używanie
umożliwa wskazanie kilku typów dla zmiennej itp.
np.
function combine(input1 number | string, input2) {
}
W TS możemy ustawlić literalną wartość dla zmiennej. Jest to przydatne jako element syngatury funkcji
funciton combine(someparam: 'first-value' | 'second-value') {}
Od teraz TS będzie pilnował czy przypadkiem nie zrobliliśmy literówki w parametrze!
Umożliwa ukrycie pod aliasem bardziej skomplikowany typ np. Union czy literał
type Combinable = numer | string;
type SomeLiteral = "someliteral" | "otherliteral";
Dodatkowo możemy tworzyć własne typy w TS!
type User = { name: string, age: number };
const u1: User = { name: "Max", age: 30 };
W TS możemy ustalić zwracany typ poprzez funkcje, jeśli tego nie zrobimy to TS automatycznie domyśli się jaki to powinnien być typ.
function something(): number {
return 1;
}
Jeżeli funkcja nie zwraca niczego powinna być void
Uwaga! Funkcja nie może zwracać typu undefined, w takim przypadku powinnien być to typ void. Co ciekawe możemy typować zmienną jako undefined (zamiast void)
W TS możemy oznaczyć zmienną jako funkcje (przechowuje referencje do funkcji)
Możemy to określić jako "ogólnie" funkcje, natomiast to nie gwarantuje że to będzie dokładnie taka funkcja (o tej sygnaturze)
let someFunction: Function;
W inny przypadku możemy określić synature funkcj jaka możemy przypisać do zmiennej
let someFunction: (a: number, b: number) => number;
Aby przekazać callback w sygnaturze funkcji, robimy tak jak poniżej
function someFunction(a: number, b: number, cb: (a: number) => void) {
cb(a);
}
Jest podobny do typu any, przyjmuje dowonlną wartość natomiast posiada znaczącą różnicę. Nie może być przypisany do innej zmiennej która posiada już jakiś typ, takie lekkie zabezpiecznie
let ui: unknown;
let ux: string;
ui = 5; // ok
ui = "Max"; // ok
ux = ui; // blad komplilacji, nie można przypisać unknown do string!
Jest to typ oznaczający że funkcja nigdy niczego nie zwraca, ale w sensie że nie wykonuje się zupełnie np.
function someFunction(): never {
throw Error("some error, function never execute properly");
}
Jest to tylko pomocne oznaczenie takie przypadku, dosyć rzadkiego
Jest to specjalny typ istniejący tylko w TS
interface SomeInterface {
name: string;
age: number;
greet(phrase: string): void;
}
let user: User;
Używamy do do opisu obiekty, alternatywą jest customwy typ ale to są odrębne koncpecje
- customowy typ - opisuje typ danych
- interfejs - opisuje obiekt
- Interejsy nie są tłumaczone do vanilla js, to byt istniejący tylko w TS
interface SomeInterface {
// w TS interfejs może zawierać pola oraz metody
someMandatoryField: string;
}
class SomeClass implements SomeInterface {
someMandatoryField: string; // musimy przykryć interfejs
}
let someObject: SomeClass; // mamy pewność że obiekt posiada metody interfejsu
Możemy zdefinować w interfejsie atrybut jako read only, co powoduje że nie będzie możliwości zmiany
interface SomeInterface { // w TS interfejs może zawierać pola oraz metody
readonly someMandatoryField: string;
}
class SomeClass implements SomeInterface {
someMandatoryField: string; // musimy przykryć interfejs
}
let someObject: SomeClass; // mamy pewność że obiekt posiada metody interfejsu
someObject = new SomeClass();
someObject.someMandatoryField = 'someValue'; // bład kompilacji
Interfejs może dziedziczyć po innym interfejsie
interface SomeInteface1 {
name: string;
}
interface SomeInterface2 extends SomeInteface1 {
surname: string;
}
interface SomeFunctionInterface {
(a: number, b: number): number;
}
- znak ? dla parametrów
- znak ! dla metod
interface SomeInterface {
optionalParam?: string,
optionalMethod! => (a: number): number
}
Możemy połczczyć różne typy w TS
type Admin = {
name: string,
access: boolean,
};
type Employee = {
position: string,
};
type ElevatedEmployee = Employee & Admin;
Możemy uzyskać podobny efekt poprzez interfejsy, natomiast w takim przypadku mamy inny operator łączenia
interface Admin = {
name: string,
access: bool
}
interface Employee = {
position: string
}
interface ElevatedEmployee extends Employee, Admin; // połączenie dwóch interfejsów
Operator działa inaczej w przypadku typów prostszych, w takim przypadku szuka wspólnej części tych zmiennych
type Combinable = string | number;
type Numeric = number | boolean;
type Universal = Combinable & Numeric; // będzie to typ numeric, bo to jest wspólne
Strażnik typu to podpowiedź dla TS że sprawdzamy czy rzeczywiście zmienna jest tym czym powinna być
Pierwszy guard - typeof
type Combinable = string | number;
type Numeric = number | boolean;
type Universal = Combinable & Numeric;
function add(a: Universal, b: Universal) {
if (typeof a === "string" || typeof b === "string") {
// to jest strażnik typu, bez tego dostalibyśmy bład kompilacji. TS domyśla że przypadek stringowy rozwaliłby nam kodzik
return a.toString() + b.toString();
}
return a + b;
}
Kolejny guard - składnia 'jakisAtrybut' in object
type Admin = {
name: string,
access: boolean,
};
type Employee = {
position: string,
};
type UnknownEmplyee = Employee | Admin;
function printEmplyee(a: UnknownEmplyee) {
if ("access" in a) {
// type guard, zapewniamy TS że to zadziała
console.log(a.access);
}
}
Ostatnią opcją jest aby użyć instanceof. Uwaga! To zadziała tylko jeśli to jest istniejąca klasa w kodzie. Nie zadziałą to dla składki TS, np. typu, czy interfejsu
instanceof NazwaKlasy
Możemy TS podpowiedzieć typ poprzez wspólne pole np. type. Jest to technika Discriminated Unions
interface Bird {
type: "bird";
flyingSpeed: number;
}
interface Horse {
type: "horse";
runingSpeed: numer;
}
type Animal = Bird | Horse;
function moveAnimal(animal: Animal) {
let s;
switch (
animal.type // istotne dla TS, rozpoznanie jakie to typ.
) {
case "bird": // Uwaga! TS nawet rozpoznaje jeśli zrobimy literówke w typie! Super!
console.log(animal.flyingSpeed); // jest ok, TS wie że to odpowiedni obiekt
case "horse":
console.log(animal.runingSpeed);
}
}
W TS możemy castować typ
// pierwszy sposób castowania elementu <NazwaTypu>
// Uwaga! Ten sposób nie jest przyjazny w aplikacjach reaktowych, może być trakotwane jako component..
const userInputElement = <HtmlInputElement>document.getELementById("someIdElement")!;
// drugi sposób castowania elementu as NazwaTypu
const userInputElement = document.getELementById("someIdElement")! as HtmlInputElement;
userInputElement.value = 'Hiii';
// kolejna alternatywa, skrótwa do castowania jak powyżej
(userInputElement as HtmlInputElement).value = 'Hiii';
W przypadku kiedy mam potrzebe zdefinoiwania obiektu który może posiadać różne atrybuty ale konkretnego typu
interface ErrorContainer { // chcemy aby mógł posiadać pola typu: email: 'błedny email', username: 'błedne znaki' itp
[props: string]: string // tutaj określamy że może posiadać WIELE LUB WCALE atrybutów ale MUSI być klucz string oraz wartość string
id: string; // jeśli chcemy dodać konkretne pole to musi się zgadzać z tym powyżej!
something: number; // BŁAD KOMPILACJI, nie zgadza się z dynamicznym polem
}
Przeciązanie funkcji (tak jak w c++)
type Combinable = string | number;
function add(a: number, b: number): string;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b:number): string; // przeciażenie syngnatury funkcji, number będzie też pasował do implementacji
function add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
// to jest strażnik typu, bez tego dostalibyśmy bład kompilacji. TS domyśla że przypadek stringowy rozwaliłby nam kodzik
return a.toString() + b.toString();
}
return a + b;
}
W przypadkiu kiedy nie wiemy czy dany atrybut istnieje możemy poinstruować TS że dane pole może nie istnieć na obiekcie
const fetchedUserData = {
id: "u1",
name: "Max",
// job: { title: 'CEO' } // w takim przypadki będzie ok, TS został poinformowany że pole może nie istnieć, natomaist może się pojawić poźniej
};
fetchedUserData?.job?.title; // ?. oznacza że pole może nie istnieć. Jeśli pole nie istnieje to przerywa łańcuch
// odpowiednik w vanilla JS, sprawdzenie czy dane istnieją
if (fetchedUserData.job && fetchedUserData.job.title) {
// coś tam zrób
}
W przypadku kiedy chcemy aby TS przypisał konkretną wartość w przypadku kiedy wartość jest nie ustawiona (null, undefined)
const userInput = undefined; // w przypadku "" (pusta wartość) to przejdzie dalej!
const storedData = userInput ?? "DEFAULT"; // przypisz wartość DEFAULT jeśli userInput jest nie ustawiony
https://www.typescriptlang.org/docs/handbook/generics.html
Typ generyczny to typ który jest silnie powiązany z innym typem. Główny typ finalnie zwraca swój powiązany typ, np Array złożny z string'a (Array) albo promise który zwraca string (Promise)
Generyczne typy dają nam to elastyczność z bezpieczństem typu
Uwaga! W przypadku array mamy ten sam zapis dla:
Typ Array<string> = string[]
Typ generyczny pozwala przewidzieć co będzie wynikiem np.
let someResult = await function somePromise(): Promise<string>
someResult.split(' '); // to działa! TS wie że zwrotką będzie string z promise (po await, czyli resolve)
W przypadu kiedy łączymy dynamiczne obiekty możemy to zdefinować jako generycznye typy
function merge<T, U>(objA: T, objB: U) {
// Uwaga! gdybyśmy tego nie zrobili i zwracali zwykły obiekt spowodowałoby to bład kompilacji
return Object.assign(objA, objB);
}
const mergedObj = merge({ name: "Max" }, { age: 30 }); // TS automatycznie uzupełnia generyczne typy zgodnie z tym co wstawiliśmy do parameterów
mergedObj.name; // brak błedu komplikacji, TS wie że to będzie obiekt jakiegoś dynamicznego typu
// możemy też zdefiniować dla TS jakiego typu będą parametry przekazane do generycznej funkcji
const mergedObj1 = merge<string, number>(...);
Możemy wymusić z jakiej "rodziny" typów musi być wskazany generyczny parametr
// parametr T MUSI być obiektem
function merge<T extends object, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
interface Lengthy {
length: number;
}
function counterAndDescribe<T extends Lengthy>(element: T): [T, string] {
let dt = "Got no value";
if (element.length === 1) {
dt = "Got 1 element";
} else if (element.length > 1) {
dt = `Got ${element.length} elements`;
}
return [element, dt];
}
counterAndDescribe('some text'); // jest ok, string posiada właściwość length
Przykład kiedy mamy funkcję generyczną która zwraca klucz dynamicznego obiektu
function extractAndConvert<T: extends object, U extends keyof T>(obj: T, key: U) {
return `Value: ` + obj[key];
}
extractAndConvert({}, 'name');
Tak jak funkcje, możemy tworzyć też generyczne klasy.
class DataStorage<T> { // gdzie T może być np. string, object itp.
private data:T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}
const textStorage = new DataStorage<string>{}; // przychowujemy string'i
const numberStorage = new DataStorage<number>{}; // przychowujemy number'y
Typ Partial pozwala nam na "tymczasowe" zbudowania pustego obiektu jakiegoś konkretnego obiektu. Tak aby następnie dynamicznie dodać niezbędne pola. Może być przydatne przy tworzeniu obiektu w builderze itp.
interface CurseGoal {
title: string;
description: string;
completeUntil: Date;
}
function createCourseGoal(
title: string,
description: string,
completeUntil: Date
): CourseGoal {
let courseGoal: Partial<CourseGoal> = {}; // To działa! TS ma obiecane że finalnie z tego powstanie obiekt CourseGoal
courseGoal.title = title;
courseGoal.description = description;
courseGoal.completeUntil = completeUntil;
return courseGoal as CourseGoal; // Uwaga! Tutaj musimy castować ponieważ to nadal był Partial
}
Readonly pomaga nam zablokować dane na jakiekolwiek zmiany
const names: Readonly<string[]> = ["Max", "Anna"];
names.push("Something"); // nie zadziała, names jest tylko do odczytu
Jest znacząca różnica pomiędzy typem generycznym a unionem.
Co istotne union powoduje że typy mogą być wymieszane!
class UnionDataStorage {
private data: (string | number | boolean)[] = []; // w tym przypadku możemy mieszać typy!
}
class GenericDataStorage<T> {
private data: (T)[] = []; // w tym przypadku mozemy ustalić że będziemy mieć tylko konkretny typ! To jest lepsze!
}
Dekoratory to funkcje które dodajemy do klas, "dekorujemy"
Odpala się w momencie jak klasa jest inicowana
Jest to wykorzystywane w np. Angularze do generowania templatki (podpiętę pod klase komponentu)
Jest to forma "meta programming" czyli dodawanie kolejnch warstw logiki poprzez dekoratory
Uwaga!
Dekoratory wymagają w tsconfig.json opcji:
- "target": "ed6"
- "experimentalDecorators": true -> odkomentować w konfiguracji
przykład dekoratora
function Logger(constructor: Function) {
// constructor to funkcja konstruktora z klasy, możemy jej użyć aby utworzyć instancje klasy do której jest podpięty dekorator
const someNewObj = new constructor();
console.log("Logging...");
}
@Logger
class Person {
name = "Max";
constructor() {
console.log("Someting...");
}
}
możemy też inaczje zapisać dektoratora, w taki sposób aby móc go sparametryzować
function Logger(someStringParam: string) {
return function (onstructor: Function) {
// w tym przypadku zwracamy funkcje dektoratora
console.log("Logging...");
};
}
@Logger("Some passed value") // teraz możemy sparametryzować dekorator!
class Person {
name = "Max";
constructor() {
console.log("Someting...");
}
}
bardziej praktyczny robudowany przykład
function withTemplate(template: string, hookId: string) {
return function (constructor: any) {
const hookEl = document.getElementById(hookId);
const p = new constructor();
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = p.name;
}
};
}
@withTemplate("<h1>asdas</h1>", "some-selector") // teraz możemy sparametryzować dekorator!
class Person {
name = "Max";
constructor() {
console.log("Someting...");
}
}
Możemy dodawać wiele dektoratów do klasy
Co istotne:
- Dekoratory tworzą się w kolejności od góry do dołu
- np. inicjacja
- Dektoratory wykonują się w kolejności od dołu do góry
- już body funkcji dektoratora
function Logger(someStringParam: string) {
console.log('LOGGER'); // to wykona się pierwsze w kolejności!
return function (constructor: Function) {
console.log('INSIDE LOGGER'); // to wykona się w czwartej kolejności
// w tym przypadku zwracamy funkcje dektoratora
console.log("Logging...");
};
}
function withTemplate(template: string, hookId: string) {
console.log('TEMPLATE'); // to wykona się w drugiej kolejności
return function (constructor: any) {
console.log('INSIDE TEMPLATE'); // to wykona się w drugiej kolejności
const hookEl = document.getElementById(hookId);
const p = new constructor();
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = p.name;
}
};
}
@Logger("asdas")
@withTemplate("<h1>asdas</h1>", "some-selector") // ten dekorator jest pierwszy!
class Person {
name = "Max";
constructor() {
console.log("Someting...");
}
}
Dekoratory też możemy dodawać do
- atrybutów klas
- metod
- parametrów metod
Uwaga! Wszystkie poniższe dekoratory odpalają się w momencie jak DEFINIUJEMY KLASE
Celem jest odpowiednie przygotowanie klasy zanim rozpocznie się wykonywanie kodu, tzw. udekorowanie klasy
// target any ponieważ nie wiemy co to będzie
// propertyName to nazwa właściwości
function Log(target: any, propertyName: string | Symbol) {
console.log('dekorator atrybutu: ' + propertyName);
}
// można dodać do dowolnej metody
funciton Log2(target: any, name: string | Symbol, descriptor: PropertyDescriptor) {
console.log('dekorator akcesora: ' + name)
}
// można dodać do dowolnej metody
// position -> to jest pozycja argumentu w sygnaturze metody
funciton Log3(target: any, name: string | Symbol, position: number) {
console.log('dekorator akcesora: ' + name)
}
class Product {
@Log
title: string;
private _price: number;
@Log2
set price(val: number) {
this._price = 0;
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@Log2
someMethod(@Log3 someParam: string) {
console.log('doing nothing');
}
}
Dekoratory też mogą zwracać wartość w postaci nowego obiektu. Pozwala to dekorować obiekt customową logiką. A nawet zwrócić inny obiekt np. jakiegoś interfejsu.
Dekoratory które mogą zwracać wartość to podpięte do
- klasy
- metod
- tutaj możemy zwrócić inny obiekt property descriptor i zmienić w jaki sposób zachowuje się metoda.
Oczywiście inne mogą też zwracać, ale nie będzie to brane pod uwagę
function withTemplate(template: string, hookId: string) {
console.log('TEMPLATE'); // to wykona się w drugiej kolejności
return function<T extends { new (...args: any[]): {name: string}}> (originalConstructor: any) { // { name: string } po to aby TS wiedział że obiekt będzie posiadał pole name
return class extends originalConstructor {
constructor(..._: args) { // zmienna to _ aby TS nie przyczepiał się do tego że używamy tego parametru
super(); // inicujemy parenta czyli nasz orginalny obiekt
// tutaj customowa logika
console.log('INSIDE TEMPLATE'); // to wykona się w drugiej kolejności
const hookEl = document.getElementById(hookId);
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = this.name;
}
}
}
};
}
@withTemplate("<h1>asdas</h1>", "some-selector") // ten dekortor kompletnie zmienia naszą klasę! wow
class Person {
name = "Max";
constructor() {
console.log("Someting...");
}
}
Wykorzystanie dektoratora do naprawy problemu scop'u this w podpiętym evencie
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() { // nadpisujemy metodę get tej metody
const boundFn = originalMethod.bind(this);
return boundFn;
},
};
}
class Printer {
message = "this works";
@Autobind
showMessage() {
console.log(this.message);
}
}
const p = new Printer();
const button = document.querySelector('button')!;
button.addEventListener('click', p.showMessage); // to zadziała!
// w vanilla js musielibyśmy zrobić tak jak poniżej, dektorator to za nas naprawia
// button.addEventListener('click', p.showMessage.bind(p));
Dektoratory dla walidacji - przykład
To jest książkowy przykład użycia dekoratorów. To mogłoby być zewnętrzna biblioteka która umożliwia Ci udekrowanie klasy odpowiedniami constraintami
przykład biblioteki: https://github.com/typestack/class-validator
function Required() {}
function PositiveNumber() {}
function validate(obj: object) {}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}
// poźniej w kodzie np.
validate(formData); // co odpali logikę walidacji, w zależności od konfiguracji walidatorów
- JavaScript Modules (Overview): https://medium.com/computed-comparisons/commonjs-vs-amd-vs-requirejs-vs-es6-modules-2e814b114a0b
- More on ES Modules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
Moduły to sposób na podzielenie swojego kondu na wiele plików, tak aby potem to includować.
sposoby:
- Namespaces and file bundling
- tylko w TS
- grupowanie po namespace
- importowanie po namespace
- per plik lub bundluje wszystkie pilki do jednego wspólnego (automatycznie)
- ES6 imports/Exports - ES6 modules
- niezalezne od TS, vanilla JS
- dostępny w wielu przeglądarkach
- przeglądarki samodzielnie importują potrzebne pliki
- wspierane przez TS
- per plik ale wystarczy jeden import (script tag)
- potrzebny jest webpack aby uzyskać bundle (jeden plik) zamiast extra requestów po pliki
Raczej nie zalecane, lepiej używać ES6 importów z webpackiem. To może być wartościowe dla mniejszych projektów w TS.
Przykład wykorzystania namespaców w TS
np. plik some_interesting_class.ts
namespace App { // Uwaga musimy mieć ten sam namespace!
export class SomeInterestingClass { // Uwaga musimy mieć export aby typ był widoczny, tylko w przypadku kiedy chcemy aby było widoczne
public someName: string;
}
}
następnie mamy nasz główny plik app.ts
// poniżej specjalna składnia importu, znaki /// to specjalny zapis dla TS
/// <reference path="some_interesting_class.ts">
namespace App { // Uwaga musimy mieć ten sam namespace!
class SomeClassUsingInterestingClass {
public someInterestingAttr: SomeInterestingClass; // to możliwe bo TS widzi ten plik w imporcie
}
}
Uwaga! Aby nie mieć problemów z ładowaniem plików po stronie przeglądaki musimy złaczyć nas wynik do jednego pliku. Możemy to zrobić poprzez ustawienie w konfiguracji
- outFile -> np. /dist/bundle.js
- Uwaga! ten plik będziemy następnie ładować po stronie HTML (script tag)
- module -> amd
Minusem namespace w TS jest to że plik includowany w innym miejscu może nam dać złudzenie że nie musimy tego importować w innym. Nie ma technicznego wymogu aby importować to co realnie jest używane w pliku. Natomiast może to potem prowadzić do skomplikowanych bugów gdzie usunięcie jednego reference popsuje inny plik który na tym polegał.
W tej styuacji jest znacznie prościej.
Uwaga! Domyślnie to będzie działać tylko w najnowszych przeglądarkach (wspierające ES6). Przeglądarka automatycznie dociągnie brakujacy plik poprzez request HTTP.
Aby to uruchomić dla starszych przeglądarek musimy dodać do naszego stacku Webpacka (spakuje wszystko w jeden bundle)
Przykład wykorzystania namespaców
np. plik some_interesting_class.ts
export class SomeInterestingClass { // wystarczy sam export!
public someName: string;
}
następnie mamy nasz główny plik app.ts
// poniżej specjalna składnia importu, znaki /// to specjalny zapis dla TS
/// <reference path="some_interesting_class.ts">
import { SomeInterestingClass } from 'some_interesting_class.js'; // Uwaga! tutaj końcówka musi być js, finalnie to będdzie ładowane przez przeglądarke
class SomeClassUsingInterestingClass {
public someInterestingAttr: SomeInterestingClass; // widoczne po imporcie
}
Następnie w opcjach TS musimy ustawić:
- module -> ES2015
oraz przy tagu script który ładuje aplikacje dodać type="module"
<script type="module" src="/dist/app.js"></script>
Dużą zaletą takiego podejścia jest to że teraz każdy plik musi samodzielnie importować wymagane zależności. Mniej dziwnych bugów.
Dodatkowo możemy:
- zgrupować importy z pliku do jakiegoś agregatora np.
import * as MyPackage from "some-file.js";
new MyPackage.SomeExportedClass(); // używamy po kropce
- Wykonać rename importu tylko w konteście tego pliku
import { SomeExportedClass as RenamedExportedClass } from "some-file.js";
new RenamedExportedClass();
- Wykonać default export aby dać znać który obiekt będzie domyślnie importowany
some-file.js
export default SomeExportedClass {};
app.js
import SomeExportedClass from "some-file.js"; // domyślnie importowany wieć nie potrzeba { }
new SomeExportedClass();
Uwaga! Kod importowanego pliku wykonuje sie tylko jednokrotnie. Nie zależnie ile razy jest importowany!
export default SomeExportedClass {};
console.log('Jakiś log'); // zobaczymy tylko jednokrotnie, niezależnie ile razy moduł został zaimportowany
dokumentacja webpack: https://webpack.js.org/
- Webpack może nam pomóc wdrożyć importy ES6 dla starszych przeglądarek
- Webpack pozwala nam spakować (bundle) pliki tak aby uniknąć ładowania wielu plików osobno
- Okiestruje pliki zgodnie z konfiguracją
- bundluje kod, mniej potrzebnych importów
- optymalizuje kod, mniej kodu do pobrania
- łatwo rozwijalna konfiguracja
npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader
- webpack - pakiet ktory potrzebujemy
- webpack-cli - CLI do webpacka
- webpack-dev-server - hot reloading dla devowego środowiska
- ts-loader - ładowanie TS przez webpacka, jak konwertować TS do JS
- target -> es6
- Tutaj Webpack będzie wiedział do jakiej wersji JS ma dążyć
- module -> es2015
- outDir -> ./dist or inny plik
- rootDir - już nie potrzebny, webpack to przejmuje
- sourceMap -> true
- to pomaga debugować kod TS
Webpack tego nie oczekuje, to tylko sładania dla przegladrek z ES6
Dodajemy plik webpack.config.js do projektu
Nastepnie dodajemy podstawową konfiguracje
const path = require('path'); // corowy moduł, nie potrzeba instalacji
module.exports = {
entry: './src/app.ts' // gdzie zaczyna się nasz projekt, główny plik
output: {
filename: 'bundle.js'
path: path.resolve(__dirname, 'dist') // bezwzględna ścieżka do katalogu
},
devtool: 'inline-source-map', // pakuje mapy do bundle i daje nam lepsze debugowanie
module: { // jak sobie radzić z konkretnymi plikami
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/ // omijamy node_modules
}
]
},
resolve: {
extensions: ['.ts', '.js'] // co razem pakujemy
}
};
kompilujemy projekt poprzez polecenie
webpack
Pamiętamy o dodaniu linku script w HTML do bundle.js z Webpacka
Po zainstalowaniu webpack-dev-server musimy poprawić naszą konfiguracje do wersji:
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/app.ts",
devServer: {
static: [
{
directory: path.join(__dirname),
},
],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/dist/",
},
devtool: "inline-source-map",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
};
zawiera nowe rzeczy potrzebne do działania webpack dev server:
- mode -> development
- publicPath -> /dist/
wystarczy że wystartujemy serwer poprzez polecenie
webpack - dev - server;
Dodajemy nowy plik z konfiguracją dla produkcji (nazwa może być inna):
webpack.config.prod.js
Doinstalowujemy specjalny pakiet do czyszczenia dysku w momencie przeładowania projektu
npm install --save-dev clean-webpack-plugin
aktualizujemy naszą konfiguracje:
const path = require("path");
const CleanPlugin = require("clean-webpack-plugin");
module.exports = {
mode: "production", // wersja produkcyjna
entry: "./src/app.ts",
devServer: {
static: [
{
directory: path.join(__dirname),
},
],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
// publicPath: "/dist/", -> już nie potrzebne, chcemy mieć pliki na dysku a nie w pamieci
},
// devtool: 'inline-source-map', // wyrzucamy z produkcji, ale jakbyśmy zostawili to moglibyśmy debugować na prodzie
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
plugins: [
// globalne pluginy
new CleanPlugin.CleanWebpackPlugin(), // automatyczne czyszcze w momecnie rebuildu
],
};
Na sam koniec aktualizujemy komende do odpalenia webpacka z konfiguracją produkcyjną
webpack --config webpack.config.prod.js
Problemem jest to że vanilla js nie zadziała poprawnie w TS, będziemy mieć błedy ponieważ TS oczekuje kodu TS.
Uwaga! Jest to do uruchomienia bo mimo wszystko pod spodem jest JS, ale będziemy mieć błedy walidacji. Jeśli wyłączymy przerywanie kompilacji to uruchomimy mimio wszystko nasz projekt. Natomiast możemy mieć potem problemy ze zbudowaniem produkcyjnej wersji.
Rozwiązaniem jest poszukanie tzw. types dla biblioteki. np. dla lodash szukamy @types/lodash
npm install --save-dev @types/lodash
takie bibliotki to zbiory tylko typów, mają nazwy np. uniq.d.ts gdzie d oznacza że to dekorator.
Na przykład na stronie mamy zdefiniowaną zmienną globalna
var globalna = "zmienna";
Aby zadziałało nam to w TS musimy zadeklarować jej istnienie, wraz z typem jakie oczekujemy. Jeżeli to coś zewnętrznego zawsze możemy użyć typu any.
declare var globalna: string;
declare var jakasZewnetrznaZmienna: any;
https://github.com/typestack/class-transformer
npm install --save class-transfomer reflect-metadata
proste użycie
import "reflect-metadata";
import { plainToClass } from "class-transformer";
const products = [
{ title: "xx", price: 29 },
{ title: "yy", price: 33 },
];
const convertedToClasses = plainToClass(NazwaKlasy, products); // super skrócik, konwertuje do klas
https://github.com/typestack/class-validator
npm install class-validator --save
proste użycie
import { IsNotEmpty, IsNumber, IsPositive } from "class-validator";
class Product {
@IsNotEmpty()
title: string;
@IsNumber()
@IsPositive()
price: number;
// i tak dalej...
}
i następnie walidujemy
import { validate } from "class-validator";
const p = new Prodct(); // cos tutaj inicujemy
validate(p).then((errors) => {
if (errors.length > 0) {
console.log(errors);
}
});
import React, { useState } from "react";
const SomeComponent = () => {
const [someParam, setSomeParam] = useState < string > ""; // typ generyczny, możemy zdefininiować czym to będzie!
};
npm init
tsc --init
- target -> es2018
- moduleResolution -> node (nowa pozycja)
- outDir -> ./dist
- rootDir -> ./src
Następnie dodaj folder src i zacznij pisać kod!
npm install --save express body-parser
nodemon - automatycznie restartuje node.js przy zmianie plików
npm install --save-dev nodemon