fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
HOT / 23 minut

Modelowanie domeny przy użyciu TypeScripta


25.06.2021

W sieci krąży mnóstwo postów i prezentacji opisujących różne typy wbudowane TypeScripta, a także, jak na ich podstawie zbudować dziesiątki innych typów. To zaś, jakie problemy chcemy dzięki nim rozwiązać, to zupełnie inny temat. Ważniejszy, bo determinuje, PO CO mielibyśmy w ogóle dane rozwiązanie zastosować. I na tym się w poniższym tekście skupimy. Zapraszam do lektury :)

TypeScript jest jedną z tych technologii, o których sporo się mówi od dobrych kilku lat, a w ostatnim czasie w szczególności. Używamy go głównie po to, aby błędy, które mogłyby wystąpić w trakcie działania aplikacji (runtime), wyłapać wcześniej, w kompilacji (compile-time). Dzięki temu na produkcji ląduje mniej błędów. Ale dodatkowo, jeśli odpowiednio wcześnie weźmiemy to pod uwagę, będzie nam łatwiej refaktorować kod oraz poruszać się po projekcie, którego nie znamy. W efekcie łatwiej nam będzie go utrzymywać – i na tym się skupmy.

Wyobraźmy sobie, że pracujemy nad systemem, w którym wykonywanych jest wiele transakcji płatniczych. Może system do fakturowania, może bank, może pożyczki – w każdym razie hasła takie jak „pieniądz”, „kwota”, „opłata” i im podobne pojawiają się bardzo często. Zarówno na poziomie API, jak i w warstwie wizualnej.

Dość wcześnie trzeba przyjąć jakąś reprezentację dla typu określającego pieniądze. Potrzebujemy go jakoś zamodelować. Pierwszy pomysł, jaki może nam przyjść do głowy – to siup! – number. Rozwiązania proste są o tyle wartościowe, że mają niski próg wejścia i ludziom powszechnie łatwo je zrozumieć, dodatkowo zmniejszamy ryzyko tak zwanego overengineeringu, czyli przekomplikowania rozwiązania. Rozwiązania proste warto brać pod uwagę od samego początku, bo często są zwyczajnie good enough.

Budujemy więc nasz system w oparciu o pieniądze, które zamodelowaliśmy jakonumber. Tu amount: number, tam debt: numberdiscount: number. Po paru latach wystąpień number jest kilkaset, jeżeli nie więcej. Przychodzi do nas Product Owner i mówi, że nasz produkt doskonale sobie radzi na rodzimym rynku, więc czas na podbój rynków zagranicznych. W związku z czym przyjęta implicite waluta (PLN) musi zostać określona explicite w każdej funkcjonalności. Jeżeli na przykład sprzedajemy towary w Polsce, ale wysyłamy je do Czech – i Czesi chcą płacić w koronach czeskich – to siłą rzeczy musimy uwzględnić dwie waluty. To taka dość oczywista sprawa. „No to ile to będzie kosztować story pointów?” – pyta PO. Cisza trwa niepokojąco długo. „Przecież to jest dodanie jednego pola”. Cisza się przedłuża. „Na pewno w jednym sprincie nie zdążymy”. „Ok, a potraficie w przybliżeniu oszacować, jak duże jest to zadanie?”

Generalnie numbernie jest w stanie przechować informacji o walucie – i wszystkie miejsca, gdzie występuje, trzeba zaktualizować tak, aby była nie tylko kwota, ale i explicite waluta. Jest może kilkadziesiąt miejsc do zmiany, może kilkaset, a do tego jeszcze gros testów, w sumie ciężko to oszacować… Przeszukiwanie codebase po frazie number daje kiepskie wyniki, bo number, czyli liczba reprezentuje liczbę sztuk, jakie klient zamówił, albo ilość powtórzeń transakcji, albo kolejny numer na liście (na przykład pozycja na fakturze). I informacja, że number występuje 1591 razy, niewiele nam mówi.

Dodatkowo, gdybyśmy hipotetycznie namierzyli te wszystkie miejsca, gdzie występuje number, i dodali obok drugie pole currency to zaczęłaby się jazda, bo obok amount: number trzeba by dodatkowo obsługiwać currency. Wygodniej byłoby zareprezentować hajs jako jeden spójny byt, tzw. Value Object. Problem w tym, że jest o te dwa lata za późno, wszędzie mamy number, bo za wczasu nie pomyślelismy o odpowiedniej abstrakcjiNie zamodelowaliśmy naszej domeny.

Primitive Obsession

Sytuacja, w której się znaleźliśmy, powszechnie nazywana jest primitive obsession, czyli „znam typy prymitywne, nie bawię się w abstrakcje, bo po co, więc tłukę stringi i numbery”. Nie opakowuję tych danych w dodatkowe (nadmiarowe?) typy, bo przecież liczba to liczba. Prawda?

Takie podejście jest kłopotliwe z co najmniej dwóch powodów. Po pierwsze, jeśli reprezentujemy hajsiwo jako number, to na hajsiwie możemy wykonywać te same operacje co na każdej liczbie. Hajs można oczywiście dodawać i odejmować. A czy można go mnożyć? 10 zł można pomnożyć przez 10. Ale czy 10 zł można pomnożyć przez 10 zł i mieć 100 zł^2? Mnożenie przez liczbę a mnożenie przez pieniądz to inne operacje. Jest to potencjalny błąd, który – odpowiednio skonfigurowany – system typów może nam łatwo wychwycić. Część z Was pomyśli: „no przecież ja takich błędów nie popełniam”. Widząc mnożenie pieniądza przez pieniądz pewnie szybko wychwycimy, że coś jest nie halo w kodziku. Natomiast jeśli pracujemy na istniejącym, działającym produkcyjnie kodzie… i potrzebujemy zaaplikować zmianę, która dotknie wielu miejsc, przejrzenie dokładnie wszystkich miejsc, których zmiana dotyka, mogłoby zająć za dużo czasu. Ostatecznie zmieniamy więc kodzik i patrzymy, czy się kompiluje; i patrzymy, czy testy przechodzą. Mamy mieszane uczucia, nie ufamy tej zmianie – mimo że po to są testy i kompilacja, aby im ufać. No bo ta zmiana taka krzywa, może być różnie. Chyba na czas rilisa wezmę wolne…

Drugi kłopot z primitive obsession to pozbawianie kodu znaczenia. Jeśli widzę funkcję process(value: number), to wiem tyle, że to liczba. A w przypadku process(value: Money)wiem już trochę więcej. Odpowiednio długo czytając okoliczny kod, dojdziemy pewnie do tego, że chodzi o pieniądze. Tylko czy nie lepiej sobie od razu ułatwić, skoro jakiś typ trzeba i tak wpisać?

Jakie zatem jest rozwiązanie? Pamiętajmy, żeby nie przeinżynierować. Spróbujmy od stworzenia zwykłego aliasu typu:

type Money = number

I w miejscach, gdzie definiujemy pieniądze, stosujemy od tej pory Money. To wszystko. Narzut na kod jest niemalże zerowy. Definiujemy jedno źródło prawdy (znane wszem wobec Single Source of Truth) i – chyba najważniejsze – nadajemy temu typowi nowe, osobne znaczenie. Osobne niż prymityw number.

Patrząc zaś od strony technicznej – co się zmieniło?

  • jeśli będziemy chcieli zmienić typ reprezentujący pieniądz, zmieniamy jego 1 deklarację (bo teraz istnieje!) i patrzymy, w ilu miejscach kod wybucha. Liczba błędów kompilacji jest mierzalna, dodatkowo kompilator pokazuje czarno na białym linijki, którymi się trzeba zaopiekować. Możemy chociażby wyrywkowo oszacować, czy docelowe zmiany będą raczej powierzchowne, czy inwazyjne. Low hanging fruit.
  • niestety, nadal możemy mnożyć pieniądze, potęgować, całkować i kto wie co jeszcze.
  • co gorsza, liczba nóg krowy jest kompatybilna z naszym typem Money. Jedno number i drugie number. Alias typu w TypeScripcie to jedynie „alias”, czyli nowa nazwa, a pod spodem istnieje ten sam typ co wcześniej.

Być może ta niewielka zmiana – mały krok dla człowieka, ale wielki skok dla projektu ;) – jest póki co wystarczający. I nie chcemy iść dalej. Sztuka w architekturze polega między innymi na tym, aby rozwiązywać faktyczne problemy, a nie domniemane. I żeby wskutek naszych zmian rachunek zysków i strat był na plusie. Zmiana, w której narobimy się jak dzikie osły – a korzyść jest niewielka – niestety powinna zostać wycofana. Człowiek jest emocjonalnie związany z potem i znojem, wysiłkiem, jaki władował w kod… tylko że ten kod dopiero wyląduje na produkcji i inni będą go musieli utrzymywać. Miejmy z tyłu głowy, że to, co piszemy, inni będą musieli rozkminiać. I to wielokrotnie. Czasami coś musi być skomplikowane, bo tak działa biznes (essential complexity), a czasami jest to przeinżynierowane – i można to, i tamto zwyczajnie usunąć (accidental complexity). Tak więc, zanim poszarżujemy dalej, zanim zainwestujemy w kolejne rozwiązanie, przeanalizujmy, jakie są jego koszty i zyski, zanim się w nie władujemy.

Fajnie, fajnie. Tylko ta myśl, że mogę przypisać liczbę nóg krowy do typu type Money = number, jakoś nie daje spokoju…

Structural Typing

A gdyby tak stworzyć nowy typ (lub interfejs)? To jeden z pierwszych pomysłów, jakie przychodzą do głowy, zwłaszcza jeśli mamy doświadczenie z technologiami takimi jak Java czy .NET. Tworzę nowy interfejs/klasę i – dopóki go nie rozszerzam – jest on niekompatybilny z całą resztą typów. Taka cecha systemów typów nazywa się „typowanie nominalne”: jeśli dwa interfejsy (lub dwie klasy) są niekompatybilne, jeśli nie są w żaden sposób „spokrewnione”, to znaczy ani nie implementują wspólnego interfejsu, ani jedna po drugiej nie dziedziczy. Programiści backendowi początkujący w TS-ie często zakładają, że skoro TS bazuje na typowaniu statycznym, tak jak Java – to może kompatybilność interfejsów (i cała mechanika polimorfizmu) również będzie podobna do tej Javowej. A skoro o tym mówimy, to najpewniej tak nie jest :)

Bardzo istotną i szczególną cechą TypeScripta jest jego cel. W wielkim skrócie sprowadza się do: „weźmy JavaScript taki, jaki jest ładny czy brzydki, nowy czy stary, nieważne i opakujmy go typami, aby wychwytywać błędy”. Wśród kilku punktów wyszczególnionych w TypeScript Design Goals możemy przeczytać między innymi: „utrzymanie runtime’owego zachowania JavaScriptu”. To bardzo ważny punkt – strategicznym założeniem TS-a jest to, że ma NIE zmieniać sposobu, w jaki działa JavaScript. To znaczy ma nie zmieniać semantyki, ma nie dodawać nowych JS-owych konstruktów (wyjątków jest bardzo mało, na przykład dekoratory), nie tworzyć własnego środowiska uruchomieniowego, i tym podobne. Ogólnie – ma być bezpieczną, type-safe wersją JavaScriptu, która w dodatku kompiluje się do niemalże takiego samego JavaScriptu, jaki sami byśmy napisali – tyle że bez typów. I jeśli programiści używali JS-a w konkretny sposób, to TS powinien co najwyżej wyłapać błędy związane z typami, ale nie wymuszać innego podejścia, paradygmatu. A na koniec ma zniknąć :) (fully erasable).

Weźmy pod uwagę to, że w JS-ie nie ma interfejsów – i nic nie zapowiada, aby się miały pojawić. W zamian powszechnie stosowany jest duck-typing, który najczęściej sprowadza się do „macania” obiektu, czy przypadkiem może akurat ma zdefiniowaną jakąś metodę – jeśli tak, to wywołaj, a nie – to olej. Albo zdefiniowane jakieś property – jeśli tak, to bierzemy – a jeśli nie, to – „proszę, tu masz wartość domyślną”. Sprawdzanie, czy property istnieje, jest w JavaScripcie czymś powszechnym i oczywistym. W JavaScripcie, czyli w runtime. A skoro typy znikają (fully erasable), to w runtime nie ma już informacji o typach, dopóki nie stosujemy czarnej magii refleksji, która ma sens jedynie dla bibliotek/frameworków. Więc skoro informacji o typach, dostępnej w czasie kompilacji, w runtime już nie ma – i w JavaScripcie powszechne jest bezpieczeństwo typów oparte o duck typing – i TypeScript ma zachować semantykę JS-a – to o co należałoby się oprzeć? :)

TypeScript wśród swoich celów ma również typowanie „strukturalnie”, co stoi w opozycji do typowania „nominalnego”. W typowaniu strukturalnym kompilatora nie interesuje, skąd się interfejs wziął… czy to interfejs, czy typ… czy może klasa… czy coś po sobie dziedziczy, rozszerza, przecina, i tak dalej. Ważne są jedynie struktury i ich zawartość – bez względu na „pochodzenie” i „nazwy” tych struktur (łac. nominal – nazwa). Jeśli oczekuję, że dostanę obiekt, który ma metodę then() -> Promise, to przyjmę nawet papieża – o ile ten implementuje tę metodę.

Czy to oznacza, że OOP, jakie znamy, nie ma w TS-ie zastosowania? Oczywiście, że MA zastosowanie, jak najbardziej. Tylko nie na podstawie pochodzenia interfejsów, a na podstawie struktur. Polimorfizm nie jest oparty o implementowanie interfejsu, a o to, czy wymagana zawartość struktury jest spełniona, czy nie. Na przykład w poniższym kodzie:

interface Runnable {
  run(): void
}

const queue: Runnable[] = []
function schedule(runnable: Runnable){
  queue.push(runnable)
}

funkcja schedule oczekuje typu Runnable jako parametru. Ale możemy przekazać jej każdy obiekt, który będzie implementował funkcję run(). Na przykład:

const john = {
  name: "John",
  run(){
    console.log("biegam sobie")
  }
}

schedule(john);

też się nada. „Na dobre i na złe”. Jak widać, obiekt nie musi być instancją klasy ani nawet zawierać anotacji typu (jeśli jej nie ma, to uruchomi się wnioskowanie – i tak czy siak wyrażenie otrzyma jakiś typ). Wreszcie, co jest istotne, typowanie strukturalne ma zastosowanie zarówno do typów obiektowych, jak i typów prymitywnych.

W konsekwencji wszystkie poniższe typy są ze sobą w pełni kompatybilne:

type Money = number
type Debt = number
type Balance = number

Możemy nie tylko tworzyć wiele aliasów typów, wielorakie interfejsy, klasy, możemy je implementować, dziedziczyć po nich…, cuda wianki. TypeScripta obchodzi jedynie zawartość danego typu, danej struktury – i tylko na tej podstawie określi kompatybilność. Tak więc javowo-dotnetowe tworzenie osobnych interfejsów po to, aby zablokować kompatybilność, ma się do TS-a trochę nijak, bo to inna bajka.

Określiliśmy fundamentalne reguły, którymi rządzi się kompatybilność typów.

Mając tę bazę możemy już budować…

…konkretne rozwiązania!

Czas wcielić w życie wiedzę o kompatybilności typów: skupimy się na budowaniu konkretnych rozwiązań z wykorzystaniem TypeScripta.

UWAGA! Właśnie trwa nabór do 1. Edycji Programu ANF: Architektura Na Froncie!
Zapraszamy serdecznie, tam nauczysz się frontendu na Poziomie PRO!
Zapisy zamykamy 30 czerwca o 21:00!
Wpadaj do nas TUTAJ »

Opaque Types

Jeśli chcemy zablokować przypisanie zwykłego number do naszego specyficznego (to znaczy bardziej doprecyzowanego) typu Money, to możemy w ten sposób skorzystać z typowania strukturalnego:

type Money = number & { readonly type: unique symbol }

Nasz alias składa się teraz z dwóch elementów: number jako taki oraz sztuczny dodatek na potrzeby kompilatora, czyli { readonly type: unique symbol }. Z chęcią poruszyłbym temat semantyki przecięć typów oraz ich reguł kompatybilności, natomiast to zasługuje na osobny wpis (i będzie omawiane podczas Architektury na Froncie). Najistotniejsze dla nas jest teraz to, że chcąc przypisać jakiekolwiek wyrażenie do typu Money, to wyrażenie musi „spełniać wymagania” nie tylko typu number, ale i { readonly type: unique symbol }. I tego drugiego nie spełni nic poza innymi wyrażeniami Money – a o to nam dokładnie chodzi! Ta druga składowa to sztuczny obiekt z polem readonly type, którego wartością jest unikalny symbol… Brzmi grubo – nie ma co ukrywać, zwiększa próg wejścia zrozumienia kodu. W rozwiązaniu ważne jest to, że ten drugi człon jest tylko do wglądu dla kompilatora – w runtime wcale go nie będzie. Takie troszkę oszustwo, ale działa:

type Money = number & { readonly type: unique symbol }
declare let m: Money
declare let n: number

m = n // ❌ Type 'number' is not assignable to type 'Money'.
n = m // ✅

Ufff… już nie można przypisać liczby nóg krowy do Money. Dlaczego? Bo liczba (nóg krowy) nie zawiera tego dodatkowego elementu { readonly type: unique symbol }. W runtime żadne z wyrażeń go nie zawierają, ale w czasie kompilacji liczy się to, co widzi kompilator. Szczegóły tego zjawiska omówimy w Architekturze na Froncie. Kompilator widzi, że typ Money to number plus coś jeszcze. Teraz, aby stworzyć zmienną Money, potrzebujemy trochę oszukać kompilator. Jeśli podstawimy zwykłą liczbę, kompilator przecież tego nie przepuści:

const money: Money = 99.99 // ❌ Type 'number' is not assignable to type 'Money'.

Musimy zatem wymusić na kompilatorze, że w niektórych miejscach my „wiemy lepiej” niż on:

const asMoney = (value: number) => value as Money
const money = asMoney(99.99) // ✅ Money

Z byciem mądrzejszym od kompilatora (czyli poprawianiem wyników jego analiz) trzeba uważać, bo często możemy nie mieć racji i w konsekwencji błędy nie będą wychwytywane przez kompilator. To jest temat rzeka i również będzie omawiany w Architekturze na Froncie. Tutaj jednak zakres zmian jest bardzo mały i kontrolujemy go. Osiągnęliśmy to, że nie możemy typu number przypisać do Money. Pozostaje jednak otwarta kwestia dozwolonych operacji, bo nie tylko potęgowanie pieniędzy jest dozwolone: money ** money, ale i operacje na styku Money i numbermoney + 10. A to dlatego, że zablokowaliśmy jedynie przypisanie, a nie operatory.

Value Objects

Możemy pójść o krok dalej i zastosować pochodzący z DDD pattern Value Object. Jest to rozwiązanie nieco bardziej inwazyjne, bo wymaga więcej kodu i w deklaracji, i w miejscach użycia. Opiera się on na stworzeniu obiektu reprezentującego wartość. I jedynie wartość. Będzie z założenia niemutowalny – bo „10 złotych” jako wartość nie zmienia się w czasie (nominalnie inflację pomijamy :P). Nie będzie też miał swojej tożsamości (ID), czyli na przykład pieniądz.

class Money {
  private constructor(
    private value: number
  ){}

  static from(value: number){
    return new Money(value)
  }

  add(another: Money){
    return new Money(this.value + another.value)
  }

  multiply(factor: number){
    return new Money(this.value * factor)
  }

  valueOf(){
    return this.value
  }
}

Dla uproszczenia nie uzupełniliśmy go o walutę. W razie potrzeby należy dodać nowe pole plus obsłużyć je potencjalnie we wszystkich metodach. I to jest cała idea Value Object – obsługa struktury danych zostaje zamknięta (zaenkapsulowana) w strukturze danych – i nie wycieka do komponentu, kontrolera czy gdziekolwiek indziej. Traktujemy wartość razem z jej regułami jako spójną całość.

Zerknijmy, jak nasz VO radzi sobie w akcji:

const m = _Money.from(99.99) // deklaracja
m + 4 // ❌ Operator '+' cannot be applied to types 'Money' and 'number'.
const n: number = _m // ❌ is not assignable to type 'number'
const sum = _m.add( _Money.from(1.23) ) // ✅ Money
const product = _m.multiply( 2 ) // ✅ Money

Całkiem nieźle. Zabezpieczyliśmy niechciane podstawienia oraz niechciane operacje. Jednocześnie umożliwiliśmy mu tylko te operacje, które w naszym biznesie są dozwolone.

Nie sposób nie zauważyć, że to rozwiązanie w porównaniu z aliasami typów jest znacznie bardziej inwazyjne. Kiedy warto je stosować? O tym zaraz.

Aplikacja, domena i typy

Wracając do tematu rozszerzenia pieniędzy w całej aplikacji tak, aby obejmowała również różne waluty, prędzej czy później pojawia się pytanie: kiedy należy stosować aliasy, opaque types, VO, a kiedy jechać na prymitywach? Jednoznacznej odpowiedzi nie ma, natomiast można wyróżnić kilka wskazówek-pytań, które pomogą nam określić, czy pozostajemy z typem prymitywnym, czy lepiej zamodelować to jako osobny typ, explicite:

  • czy to, co otypowaliśmy jako string, to faktycznie dowolny string? Na przykład imię firstName: string – wprawdzie istnieje skończona liczba imion (przynajmniej w Polsce), ale bez przesady – imię to po prostu tekst, który nie ma swojego dodatkowego znaczenia. Albo komentarz comment: string – to po prostu tekst. Ale na przykład stanowisko pracownika – position? Być może pierwotnie będzie to po prostu string z wartościami JavaScript Developer. Ale jeśli pracujemy nad systemem kadrowym, w którym chcemy domenowo rozróżniać Senior od Junior,to z czasem stanowisko może przestać być stringiem i stać się obiektem z kilkoma polami – { title: string, level: ENUM }. I alias będzie jak znalazł.
  • czy element, który chcemy otypować, funkcjonuje samodzielnie w naszym biznesie? Przykładowo w aplikacji operującej na finansach pieniądz pojawia się wielokrotnie i nie jest po prostu liczbą taką, jak każda inna. Bo ma swoje dodatkowe znaczenie W aplikacji HR-owej umiejętność JavaScript to nie musi być po prostustring. JavaScript, który ktoś zna od roku – i JavaScript, który ktoś trzaska od 10 lat w wielu projektach – to nie to samo. I prędzej czy później biznes będzie chciał to rozróżnić. Choćby dlatego, że kontraktornia, podsyłając CV kandydatów „grubemu globalnemu graczowi”, chce posortować malejąco kandydatów według skomplikowanych kryteriów. A kandydatów jest sporo.

Generalnie, nie jesteśmy w stanie z góry przewidzieć, które typy w przyszłości się zmienią. Rozsądne zatem wydaje się zacząć z aliasem typu tam, gdzie ma on samodzielne biznesowe znaczenie (na przykład pieniądz) – i rozszerzać go w przyszłości, kiedy pojawi się wymaganie biznesowe. Lub jeśli programiści zauważą, że często zdarzają się bugi, które na poziomie TypeScripta dałoby się rozwiązać. Ta mała inwestycja – ale zaaplikowana odpowiednio wcześnie – kosztuje bardzo mało, a zapewnia elastyczność. Nie mówiąc o łatwiejszym rozumieniu kodu – jeśli w kodzie jest Money, a nie number, to nie tylko więcej wiem o tych danych, czytając kod – ale także łatwiej mi wyszukać wystąpienia tego typu.

Miejmy zawsze z tyłu głowy, że wypuszczenie kodu na produkcję to dopiero początek potencjalnie długiego życia tego kodu. Jeśli pracujemy w agencji interaktywnej i tworzymy apkę reklamową z czerwoną ciężarówką słodkiego napoju, pędzącego na tle zimowej scenerii – to po świętach nasza apka zostanie zaorana i świat o niej zapomni. Długoterminowe inwestowanie w jakość… nie ma sensu. Ale jeśli tworzymy systemy biznesowe, to ktoś będzie je z większym lub mniejszym bólem utrzymywał. Perspektywa łatwego tworzenia kodu – i perspektywa łatwego jego późniejszego utrzymania – to często dwie różne perspektywy. Implementowanie aliasu zamiast prymitywa może nam nie zrobić wielkiej różnicy w momencie kodowania. Ale jeśli będziemy chcieli zmienić go kilka lat później, to dopiero wtedy się okaże, czy task kosztuje jeden tydzień czy dwa miesiące.

Podsumowanie

Omówiliśmy, po co, kiedy, kiedy nie i w jaki sposób można stworzyć alias typów reprezentujących dane domenowe. Poruszony tu temat to zaledwie wierzchołek góry lodowej możliwości TypeScripta :).

Aby wejść głębiej w zagadnienia type-safety, projektowania aplikacji i wielu innych wątków frontendowych, zapraszam na Architekturę na Froncie.

0 0 votes
Article Rating
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Łukasz
Łukasz
2 years ago

Modelowanie domeny w TS na stworzeniu (lub wybraniu) odpowiedniego typu właściwości ? Jeśli tak to chyba są odpowiednie biblioteki które rozwiązują problem typów currency w JS?
Poza tym dlaczego mam używać TS dla projektowania domeny, nie lepiej jakiś C# ?

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również