Joi — walidacja danych

Ten wpis jest 5 częścią z 5 w kursie HapiJS

Walidacja danych to bardzo ciekawy i rozbudowany temat, a ja znalazłem swoją ulubioną paczkę do tego 🙂 Ten wpis poświęcam w 100% bibliotece Joi. Wbrew pozorom, nie jest to wcale tak banalna sprawa, jakby się mogło wydawać! Joi służy do walidacji danych w Node.js. Można jej używać z dowolnym frameworkiem, ale, co dla mnie istotne, jest mocno zintegrowana z HapiJS!

O Joi…

Dość nietypowo: wpis zaczynam od podsumowania. Chcę w ten sposób zachęcić Cię do bliższego poznania Joi, bo możliwości, które daje ta biblioteka są praktycznie nieograniczone. Gdy pierwszy raz zacząłem jej używać byłem pod ogromnym wrażeniem jak łatwe jest budowanie nawet najbardziej skomplikowanych zasad walidacji i nie tylko…

A więc:

  • Joi to biblioteka do walidacji danych — sprawdza czy dane pasują do podanego formatu
  • Joi może być bez problemu używana z dowolnym frameworkiem (mimo że jest ściśle zintegrowane z HapiJS)
  • Możesz użyć Joi w HapiJS do automatycznej walidacji elementów żądań: nagłówków, querystring, parametrów czy ciała (body)
  • Możesz także walidować swoje odpowiedzi, aby np. uniknąć przesłania do użytkownika danych, które powinny pozostać tajne
  • Joi daje łatwą możliwość precyzowania własnych komunikatów błędów

Zainteresowana/y? To jeszcze nie wszystko 😍 Ale zacznijmy od początku…

Walidacja w ogóle

Walidacja danych pochodzących od użytkownika (żądań) ma zasadniczo dwa cele. Po pierwsze pomaga programiście upewnić się, że użytkownik podaje prawidłowe dane, aby aplikacja mogła dalej działać. Przykładowo jeśli w formularzu oczekiwany jest wiek, a użytkownik wpisze tam rok urodzenia, Joi może taki przypadek wykryć, dzięki czemu aplikacja nie zapisze niepoprawnych danych. Jest tutaj też ogromna korzyść dla użytkownika: Dostaje jasny komunikat o błędzie i wie dokładnie co musi poprawić.

Drugi aspekt to bezpieczeństwo aplikacji. Walidacja pozwala Tobie (programiście) upewnić się, że gdy ktoś będzie próbował złośliwie przekazać spreparowaną treść żądania, to aplikacja nie zachowa się w sposób niebezpieczny — np. nie ujawni tajnych danych, albo nie pozwoli na atak SQL Injection lub inny. To ryzyko można w dużej mierze ograniczyć dzięki poprawnie skonfigurowanej i możliwie jak najbardziej ścisłej walidacji.

Joi idealnie sprawdza się do obu przypadków.

Joi w Node.js

Joi może być używana w samodzielnie, bez HapiJS. Praca z Joi sprowadza się do dwóch kroków:

  1. Stworzenie schema, czyli obiektu, który opisuje Twoje oczekiwania wobec danych.
  2. Sprawdzenie czy przychodzące dane pasują do danej schema’y.

Najprostszy możliwy przykład. Oczekujemy, że zmienna zawiera wiek — czyli liczbę pomiędzy 18 a 130 😉 Tak wygląda schema w Joi:

const schema = Joi.number().min(18).max(130);

Natomiast sam proces walidacji sprowadza się do wywołania jednej funkcji:

const age = 20;
Joi.assert(age, schema);

Gdyby do stałej age przypisać liczbę poniżej 18 lub powyżej 130, wywołanie Joi.assert zakończyłoby się rzuceniem przez bibliotekę wyjątku:

const age = 12;
Joi.assert(age, schema); // Error!

Wow, to było łatwe, prawda? Joi zawiera analogiczne, proste i intuicyjne metody dla wielu innych typów, a między innymi dla number, string, array, date czy object. Przykład dla obiektu:

const userSchema = Joi.object().keys({
  id: Joi.number().required(),
  …
});

Dla ułatwienia, można też nie pisać Joi.object().keys(…), wystarczy tylko sam literał obiektu. Jeszcze bardziej rozbudowany przykład:

const userSchema = {
  id: Joi.number().required(),
  name: Joi.string().required(),
  address: Joi.string(),
  language: Joi.string().valid(['pl', 'en']).required()
};

Ostatnie pole w obiekcie, language, ma ściśle zdefiniowane poprawne wartości. W przypadku błędu, automatycznie generowana informacja zwrotna będzie bardzo pomocna:

[1] language must be one of [pl, en]

Zwróć też uwagę, że domyślnie wszystkie pola w obiekcie są opcjonalne. Aby to zmienić, dodaję do schema wywołanie metody .required().

Sprawdzanie czy dane pasują do schema’y

Pokazałem jeden sposób walidowania danych — poprzez wywołanie funkcji Joi.assert, która rzuca wyjątek w wypadku błędu. W wielu miejscach jest to przydatne, bo, przykładowo, HapiJS automatycznie taki wyjątek wychwyci i zwróci odpowiedź z błędem do użytkownika.

Jednak wyjątek to nie zawsze to czego możesz chcieć, ale na szczęście Joi oferuje też drugą metodę: Joi.validate(). Przyjmuje ona 3 argumenty: dane, schema i funkcję (callback w stylu Node).

Joi.validate(age, schema, (err, val) => {
  if (err) {
    console.log('Walidacja się nie udała!');
  } else {
    console.log('Wszystko w porządku.');
  }
});

Warto pamiętać, że Joi.validate wywołuje callback synchronicznie.

Joi a typy

Jeśli zaczęłaś/zacząłeś się już bawić z Joi to mogłaś/eś zauważyć, że string '123' zostanie prawidłowo zwalidowany przez Joi jako liczba. Czy to bug?! Nie! Joi.number() waliduje JavaScriptowe liczby, ale także coś co wygląda jak liczba, czyli na przykład string '567'. Czy to pożądane zachowanie?

Załóżmy, że chcesz zwalidować querystring — czyli parametry dopisywane po adresie w postaci mojadres.com?param1=abc&param2=123. Domyślnie wszystkie są traktowane przez Node jako stringi — czyli po sparsowaniu otrzymujesz coś na kształt obiektu { param1: 'abc', param2: '123' }. Jednak przecież wyraźnie drugi parametr jest liczbą!

Joi potrafi obsłużyć tę sytuację prawidłowo. Co więcej — po zwalidowaniu automatycznie dokonuje konwersji tego stringa na liczbę:

const schema = Joi.number();
const kindOfNumber = '123';
Joi.validate(kindOfNumber, schema, (err, value) => {
  // value tutaj jest już liczbą!
  console.log(typeof value); // 'number'
});

Taka konwersja typów jest również bardzo przydatna w przypadku dat. Daty przekazywane z przeglądarki do Node zawsze są stringami (lub liczbą w postaci timestampa). Joi potrafi automatycznie sparsować i przekonwertować te formaty na obiekt Date — a więc Ty możesz operować już bezpośrednio na datach 😎

Opcje Joi

convert

Jeśli powyższe zachowanie jest niepożądane to, hej, Joi można dodatkowo konfigurować 😉 Możesz to zmienić na poziomie schema’y:

const schema = Joi.number().options({ convert: false });

Lub bezpośrednio przy wywołaniu Joi.validate

Joi.validate(kindOfNumber, schema, { convert: false }, (err, value) => {
  // błąd!
});

abortEarly

Drugim zachowaniem, o którym do tej pory jakoś nie miałem okazji wspomnieć, jest to, że Joi zaprzestaje walidacji po pierwszym napotykanym błędzie. Jest to mądre podejście, bo nie ma sensu niepotrzebnie męczyć serwera — jeśli dane są niepoprawne to najczęściej chcesz jak najszybciej przestać się nimi zajmować i zwrócić błąd:

const schema = {
  id: Joi.number(),
  name: Joi.string(),
};

const clearlyNotUser = {
  id: 'lol',
  name: 12,
};

Joi.validate(clearlyNotUser, schema, (err, value) => {
  // błąd! Ale obiekt err zawiera informacje tylko o niepoprawnym id
});

Czasem może to być niepożądane. Wyobraź sobie, że dokonujesz walidacji formularza, który uzupełnia użytkownik. Nie chcesz go męczyć i zwracać mu błędy po kolei, jeden po jednym, prawda? Lepiej byłoby zwrócić wszystkie błędy na raz, aby użytkownik mógł poprawić wszystkie błędne pola. Pomoże w tym opcja abortEarly:

Joi.validate(clearlyNotUser, schema, {abortEarly: false}, (err, value) => {
  // błąd! Obiekt err zawiera informację o obu błędach
});

stripUnknown

Wspomniałem już gdzieś o tym, że Joi może Ci pomóc zabezpieczyć zwracane dane przed przypadkowym przesłaniem do użytkownika pól, które nigdy nie powinny ujrzeć światła dziennego. Wyobraź sobie taki scenariusz: W bazie danych przechowujesz użytkowników, ich imiona, nazwiska, ale też maile i hashe haseł. Każdy użytkownik może podejrzeć imiona innych użytkowników. Ale przypadkiem popełniłaś/eś błąd i Twoje API zwraca wszystkie dane na temat każdego użytkownika:

const users = [
  { name: 'Michal', password: 'abcdefghijklmnopqrstu', email: 'email@email.com' },
  …
];

Ups! Tej sytuacji można było uniknąć poprzez odpowiednie filtrowanie danych i usunięcie niepotrzebnych pól — ale to kiepska opcja, bo jeśli w przyszłości do obiektu użytkownika dojdą inne dane, których też nie chcemy zwracać, to będziemy musieli pamiętać, aby zmodyfikować ten kod, który dane filtruje. Zdecydowanie lepsza byłaby biała lista. Można by też po prostu pobierać z bazy tylko imiona. Jasne 🙂 Ale załóżmy, że ignorujemy na razie taką możliwość, albo chcemy mieć też drugą warstwę bezpieczeństwa. Pomoże nam w tym Joi!

Wystarczy użyć opcji stripUnknown:

const users = […];

const usersNameResponseSchema = Joi.array().items({
  name: Joi.string(),
});

Joi.validate(users, usersNameResponseSchema, { stripUnknown: true }, (err, value) => {
  console.log(value) // [ { name: 'Michal' } ]
});

Jak widzisz, Joi może pomóc zarówno przy bezpieczeństwie danych wejściowych, jak i wyjściowych. Jeśli spytasz mnie o zdanie — stripUnknown to moja ulubiona opcja.

Definiowanie schema

Teraz chciałbym na konkretnych przykładach pokazać jak ja definiuję swoje schema’y. Nie wiem czy to najlepszy sposób, ale na pewno dobry i przetestowany w boju 😉 Więc do dzieła!

Po pierwsze: Schema’y można deklarować raz a używać w wielu miejscach. Co mam na myśli? No na przykład wyobraź sobie, że chcesz napisać schema’y dla żądań pobrania, dodania i edycji użytkowników (np. GET, POST, PUT). Zacznij od zdefiniowania kształtu obiektu użytkownika:

const userSchema = Joi.object({
  name: Joi.string().required(),
  language: Joi.string().valid(['pl', 'en']).required(),
  address: Joi.string().optional().allow(''), // pusty string jest też poprawny
});

Następnie stwórz schema’y opisujące kolejne żądania i odpowiedzi:

const userWithIdSchema = userSchema.keys({ id: Joi.number().required() });

const createUserRequestSchema = userSchema;
const createUserResponseSchema = userWithIdSchema;

const updateUserRequestSchema = userWithIdSchema;
const updateUserResponseSchema = userWithIdSchema;

const getUserResponseSchema = Joi.array().items(userSchema);

W ten sposób raz zadeklarowany kształt obiektu użytkownika został rozszerzony (o id) i użyty wielokrotnie w różnych kontekstach (np. w odpowiedzi na GET zwracana jest tablica użytkowników). Dzięki takiemu podejściu późniejsze zmiany w obiekcie użytkownika będą wymagały modyfikacji schema’y tylko w jednym miejscu.

Nota poboczna: Fani TypeScripta pewnie teraz zastanawiają się czy to oznacza, że trzeba napisać schema i potem jeszcze interface lub type dla każdego z żądań i każdej odpowiedzi? Niestety — tak. Istnieje jednak skrypt, który pozwala to zautomatyzować i aktualnie go testuję. Jednak jest to bardzo testowa wersja, a autor ogłosił, że porzucił pracę nad nim. Jeśli jednak ostatecznie uznam ten skrypt za przydatny to możliwe, że sam przejmę jego dalszy rozwój.

Prosta logika w Joi

Ja przenoszę do Joi niektóre elementy (te proste) logiki aplikacji. Na przykład gdy użytkownik się rejestruje to proszę go o dwukrotne podanie hasła, a następnie na backendzie sprawdzam czy oba hasła są takie same. To częsty scenariusz, prawda? Napisanie tego fragmentu kodu jest proste, ale jednocześnie wydaje się wtórne. Na szczęście Joi potrafi dokonać również takiej walidacji!

Aby móc porównywać pola pomiędzy sobą musisz nadać im jakieś identyfikatory (nazwy), a potem odwołać się do nich przez funkcję ref:

const signUpRequestSchema = {
  login: Joi.string().required(),
  password: Joi.string().required(),
  repeatPassword: Joi.any().valid(Joi.ref('password')).required()
};

W ten sposób jeśli passwordrepeatPassword nie będą takie same to Joi zwróci błąd.

language

Niestety błąd ten nie jest zbyt przyjazny dla użytkownika:

"repeatPassword" must be one of [ref:password]

Szczęśliwie, Joi pozwala również na całkowitą zmianę treści błędów:

const signUpRequestSchema = {
  login: Joi.string().required(),
  password: Joi.string().required(),
  repeatPassword: Joi.any().valid(Joi.ref('password')).required().options({ language: { any: { allowOnly: 'must match password' } } })
};
"repeatPassword" must match password

W ten sposób nadpisałem komunikat dla tego konkretnego błędu. Zaletą opcji language jest to, że wszystkie komunikaty błędów można by np. zapisać sobie w pliku JSON i wczytać warunkowo — np. zależnie od języka użytkownika. Komunikaty można znaleźć tutaj: https://github.com/hapijs/joi/blob/master/lib/language.js

Możliwości budowania relacji i logiki jest jeszcze więcej, ale w tym wpisie nie ma miejsca abym opisał wszystko 🙂 Joi pozwala także na definiowanie pól wzajemnie wykluczających się albo alternatyw (and, or itp.) Więcej w dokumentacji! https://github.com/hapijs/joi/blob/master/API.md

Własne walidatory

Jeśli powyższe możliwości to nadal dla Ciebie za mało to, nie bój nic, Joi pozwala na definiowanie całkowicie dowolnych walidatorów i konwerterów! Co to takiego? Walidator sprawdza poprawność danych, a konwerter zmienia typ (jak w przykładzie z Joi.number() i stringiem '123').

Konkretny przykład, prawdziwy, z aplikacji, nad którą pracuję. Umożliwiam użytkownikom zaznaczenie kilku elementów w aplikacji i kliknięcie guzika „pobierz”. Wtedy do API idzie request z listą ID elementów oddzielonych przecinkami (aby zapis był krótki, bo tych ID może być sporo):

https://example.typeofweb.com/download?ids=1,2,15,66

Chciałbym dokonać walidacji parametru ids. Ma to być ciąg liczb oddzielonych od siebie przecinkami. Dodatkowo chciałbym, aby Joi dokonał konwersji tego ciągu na tablicę liczb. Czy jest to możliwe? Ależ tak! Wystarczy zadeklarować własne metody:

const JoiCommaDelimited = Joi.extend({
  name: 'commaDelimited',
  base: Joi.string(),
  language: {
    items: '{{error}}',
  },
  pre(value, _state, _options) {
    return value.split(',');
  },
  rules: [{
    name: 'items',
    params: {
      items: Joi.any()
    },
    validate(params, value, state, options) {
      const validation = Joi.array().items(params.items).validate(value);
      if (validation.error) {
        return this.createError('commaDelimited.items', { error: validation.error }, state, options);
      } else {
        return validation.value;
      }
    }
  }]
});

Na spokojnie przeanalizuj ten kod — nie jest taki straszny jak się początkowo wydaje 🙂 Jego wykorzystanie:

const schema = {
  items: JoiCommaDelimited.commaDelimited().items(Joi.number())
};

Joi.validate({items: '1,2,3,50'}, schema, (err, value) => {
  console.log(value) // { items: [ 1, 2, 3, 50 ] }
});

W ten sposób niezależnie od tego czy jest to ids=1 czy ids=1,2,55 — zawsze otrzymuję tablicę liczb. Ponadto, gdyby ktoś próbował przekazać tam coś innego niż liczbę np. ids=ab,2,c Joi zwróci błąd! Pięknie 🙂

Podsumowanie

Umiesz już dokonywać nawet bardzo skomplikowanej walidacji dzięki Joi. Potrafisz definiować kształt obiektów, tworzyć prostą logiką, a także korzystać z zaawansowanych możliwości definiowania własnych walidatorów. Czy już lubisz Joi? Jestem przekonany, że zgodzisz się ze mną, że jest to niezwykle przyjemna biblioteka, która na pewno przypadnie Ci do gustu 👍

  • Nie jestem mistrzem języka Polskiego, ale jeżeli „schema’y” jest poprawniejsze niż „schemy” to wypisuję się z tego języka >_>

    Dobra, nie ma zasad ustalonych[1], więc będę się powoływał na moje subiektywne poczucie estetyki które mówi, że „schemy” jest pod każdym względem lepsze 🙂

    Jeszcze druga uwaga, standardem jest używanie domeny @example.com do testowym maili [2].

    Sam Joi wygląda fajnie, natomiast nie będę oszukiwał, dodawanie własnej wiadomości błędu do powtórzonego hasła jest masakrycznie niepraktyczne.
    `Joi.any().valid(Joi.ref(‚password’)).required().options({ language: { any: { allowOnly: ‚must match password’ } } })`

    Gdy mogłoby być:

    `Joi.any().valid(Jo.ref(‚password’), „must match password”).required()`

    Ale już nie pierwszy raz zauważyłem, że nie ma czegoś takiego jak idealna biblioteka, plus standardowy aforyzm na tę sytuację: „with great power comes a huge amount of code you have to write” czy jakoś tak, jeżeli dobrze pamiętam 😉

    [1]: https://sjp.pwn.pl/poradnia/haslo/apostrof-w-wyrazach-obcych;11890.html
    [2]: https://stackoverflow.com/questions/1368163/is-there-a-standard-domain-for-testing-throwaway-email

    • Moje mówi, że zapis oryginalny z apostrofem jest lepszy pod każdym względem 🙂 Przede wszystkim nie trzeba się domyślać jaka była oryginalna forma. W zapisie `schemy` nie wiadomo czy oryginalnie było `schema` czy `scheme`.

      • Ale czy ważne jest wiedzieć jaka jest oryginalna forma? Dla mnie pierwsze skojarzenie jest „ok, schema—- wróć, schemay, co ja czytam? oh!”. Plus mimo wszystko traktowanie słowa jakby było polskie i odmienianie go z polskimi zasadami wydaje mi się ładniejsze i prostsze.

        No cóż, jak pisałem, subiektywne poczucie estetyki :).

        Komentarz wyżej aktualizowałem tak swoją drogą, jakbyś chciał się odnieść.

        • Przecież schema to jest schemat, diagram – dlaczego w tekście „object literal” to „literał obiektowy” a „schema” to „schema” z czego wychodzą potem takie potworki?

          • schema to konkretne określenie pochodzące z dokumentacji Joi, nie żaden schemat ani tym bardziej diagram.
            Próba tłumaczenia takich określeń na język polski — to właśnie tworzenie potworków. Oczywiście są pewne określenia, które już weszły w poszechne użycie, jak np. żądanie czy właśnie literał obiektu, ale „schema” czy „handler” przetłumaczone na język polski brzmią bardzo koślawo 😀

        • Tak, tylko później jak chcesz przeszukać dokumentację czy Google’a to musisz znać oryginalną pisownię 😀

          • Mógłbym się zgodzić, ale w praktyce jest to sytuacja, która raczej się nie zdarza – jedyne co znajduję pod kątem słowa scheme (nie licząc bycia synonimem do schema) to język do programowania, ale wtedy masz dwie opcje:

            1. Albo artykuł jest o tym języku i z kontekstu wiadomo o co chodzi.
            2. Albo zakładamy, że czytelnik jest tak wielkim nowicjuszem, że i tak potrzebuje w artykule dostać definicję tego, o czym jest mowa.

            Jest jakieś prawo które mówi, że mając do wyboru dwie rzeczy, jedną bardzo ważną a drugą straszną pierdołe, ludzie spędzają 99% czasu dyskutując o tej głupocie, ale nie mogę tego teraz znaleźć 😀

          • My tu gadu gadu o języku polskim, a to blog o programowaniu 😉

    • W tym prostym przypadku to mogłoby zadziałać, ale co jeśli masz walidację Joi.number().min(2).max(10)? Chciałbyś mieć różne komunikaty błędów gdy argument nie jest liczbą, gdy jest mniejszy niż 2 i gdy jest większy niż 10. Obiekt languages pozwala na nadpisanie wszystkich tych domyślnych komunikatów — jeśli potrzebujesz.

      • Wtedy możesz mieć kolejną metodę do chainowania, coś w stylu Joi.number().min(2).max(10).errorMessage("You failed as a human being today")

        • Co sprawia, że jest to mega nieczytelne, a ponadto redundantne 😉

          • Może coś ze mną jest nie tak, ale w jaki sposób

            .options({ language: { any: { allowOnly: 'must match password' } } }) jest czytelniejsze od .errorMessage("You failed as a human being today")? {wstaw emotke zastanawiania się}

          • Przykład, który rozważamy wyglądałby w Twojej wersji chyba jakoś tak:


            Joi.number().errorMessage('Podaj liczbę').min(2).errorMessage('Liczba jest za mała').max(10).errorMessage('Liczba jest za duża')

            Zdecydowanie mniej czytelne.

          • Dobra, dogadaliśmy się na FB, źle się zrozumieliśmy – ja myślałem, że ten .options() znaczy, że ten sam komunikat jest wyświetlany niezależnie która walidacja dla danego pola nie zadziała. Jeżeli ma być osobny message definiowany dla każdego pola, to nie jest to zbyt czytelne (aczkolwiek robi się czytelne jak ładnie złamiemy linie), zaś w takiej sytuacji osobiście preferuję styl taki, jak jest w praktycznie każdej bibliotece do assercji, czyli ostatni, opcjonalny argument to własny komunikat błedu, czyli np. Joi.number('Podaj liczbę').min(2, "Liczba za mała').

            Co do architekturalnego bubla też się nie mogę do końca zgodzić :D. Wywołanie number() tworzy i przypina validator typu number, następnie możesz definiować opcjonalne parametry tego właśnie validatora, po czym tworzysz kolejny validator i znowu jego dokonfigurowujesz.

            Troszkę w podobny sposób działał graphics library we flashu (aczkolwiek tam się nie chainowało) czy działa rysowanie w SVG, tj łańcuch wywołań nie jest bezstanowy, a poprzednie wywołania wpływają na stan aktualnego. Podobnież widziałem paskudnie napisane kosmosy w jQuery, które zamiast zaczynać z nową kolekcją to manipulują jak koń pod górę ale to akurat nie jest pozytywny przykład.

            I jeszcze się odniosę do tłumaczeń – rozumiem, że chodzi Ci o taki case .options({language: calyObiektGdziesTamSkadsIndziejs})? Jeżeli tak to zgadzam się, jest to bardzo wygodne 🙂

  • majkel_94

    > „Fani TypeScripta pewnie teraz zastanawiają się czy to oznacza, że trzeba napisać schema i potem jeszcze interface lub type dla każdego z żądań i każdej odpowiedzi? Niestety — tak.”

    Fani TypeScripta do walidacji używają class-validator:
    https://github.com/typestack/class-validator
    oraz używają frameworków wykorzystujących możliwości dekoratorów, jak routing-controllers lub nest.js, które się bardzo dobrze integrują z class-validator 😉
    https://github.com/typestack/routing-controllers
    https://github.com/nestjs/nest

    • Znam, ale nie używałem. I w sumie mam teraz konkretne pytanie 🙂

      Jak miałby w tym przypadku działać class-validator? Załóżmy, że dostaję obiekt request:
      {
      a: 1,
      b: 2,
      c: '123',
      }

      I już mam ten obiekt. Jak mogę go zwalidować przy pomocy class-validator? Muszę zdefiniować klasę, stworzyć jej instancję, a potem każde pole z request przypisać do instancji klasy? Czy jest jakiś inny sposób?

    • Ok, chyba znalazłem odpowiedź na swoje pytanie: Tak, trzeba.

      Ale tutaj przedstawione jest ciekawe rozwiązanie. Niemniej jednak bez pełnego stacku opartego o TypeScript, nie będzie to wygodne.