Zaawansowany TypeScript z prawdziwym przykładem (część 1)
Opublikowany: 2020-11-06W zeszłym tygodniu widzieliśmy małe wprowadzenie do TypeScript, a konkretnie rozmawialiśmy o tym, jak ten język, który rozszerza JavaScript, może pomóc nam w tworzeniu bardziej niezawodnego kodu. Ponieważ było to tylko wprowadzenie, nie mówiłem o niektórych funkcjach TypeScript, które możesz chcieć (i prawdopodobnie potrzebujesz) użyć w swoich projektach.
Dziś nauczę Cię, jak profesjonalnie zastosować TypeScript w prawdziwym projekcie. Aby to zrobić, zaczniemy od przyjrzenia się fragmentowi kodu źródłowego Nelio Content, aby zrozumieć, od czego zaczynamy i jakie ograniczenia mamy obecnie. Następnie będziemy stopniowo ulepszać oryginalny kod JavaScript, dodając małe przyrostowe ulepszenia, aż będziemy mieli w pełni napisany kod.
Korzystanie z kodu źródłowego Nelio Content jako podstawy
Jak być może już wiesz, Nelio Content to wtyczka, która umożliwia udostępnianie treści Twojej witryny w mediach społecznościowych. Oprócz tego zawiera również kilka funkcji, które mają na celu pomóc ci w ciągłym generowaniu lepszych treści na twoim blogu, takich jak analiza jakości twoich postów, kalendarz redakcyjny do śledzenia nadchodzących treści, które musisz napisać, i tak dalej .

W zeszłym miesiącu opublikowaliśmy wersję 2.0, całkowicie przeprojektowaną zarówno wizualnie, jak i wewnętrznie naszej wtyczki. Stworzyliśmy tę wersję, korzystając ze wszystkich nowych technologii, które mamy dziś dostępne w WordPressie (o czym rozmawialiśmy ostatnio na naszym blogu), w tym interfejsu React i sklepu Redux.
Tak więc w dzisiejszym przykładzie ulepszymy to drugie. Oznacza to, że zobaczymy, jak możemy wpisać sklep Redux.
Wybory kalendarzy redakcyjnych Nelio
Kalendarz redakcyjny to interfejs użytkownika, który pokazuje posty na blogu, które zaplanowaliśmy na każdy dzień tygodnia. Oznacza to, że nasz sklep Redux będzie potrzebował co najmniej dwóch operacji zapytania: jednej, która informuje nas o postach, które są zaplanowane na dany dzień, i drugiej, która po podaniu identyfikatora posta zwraca wszystkie jego atrybuty.
Zakładając, że przeczytałeś nasze posty na ten temat, wiesz już, że selektor w Redux jako pierwszy parametr otrzymuje stan ze wszystkimi informacjami o naszej aplikacji, a następnie wszelkie dodatkowe parametry, których może potrzebować. Więc nasze dwa przykładowe selektory w JavaScript wyglądałyby mniej więcej tak:
function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; } Jeśli zastanawiasz się, skąd wiem, że stan ma atrybuty posts i days , jest to całkiem proste: ponieważ to ja je zdefiniowałem. Ale oto dlaczego zdecydowałem się je wdrożyć w ten sposób.
Wiemy, że chcemy mieć dostęp do naszych informacji z dwóch różnych punktów widzenia: posty w ciągu dnia lub posty według ID. Wydaje się więc, że warto podzielić nasze dane na dwie części:
- Z jednej strony mamy atrybut
posts, w którym wymieniliśmy wszystkie posty, które pozyskaliśmy z serwera i zapisaliśmy w naszym sklepie Redux. Logicznie rzecz biorąc, moglibyśmy zapisać je w tablicy i przeprowadzić sekwencyjne wyszukiwanie, aby znaleźć post, którego identyfikator pasuje do oczekiwanego… ale obiekt zachowuje się jak słownik, oferując szybsze wyszukiwanie. - Z drugiej strony musimy również uzyskać dostęp do postów, które są zaplanowane na określony dzień. Ponownie, moglibyśmy użyć tylko jednej tablicy do przechowywania wszystkich postów i filtrowania jej w celu znalezienia postów należących do określonego dnia, ale posiadanie jeszcze innego słownika oferuje szybsze rozwiązanie wyszukiwania.
Akcje i reduktory w zawartości Nelio
Wreszcie, jeśli zależy nam na dynamicznym kalendarzu, musimy wdrożyć funkcje, które pozwolą nam aktualizować informacje, które przechowuje nasz sklep. Dla uproszczenia zaproponujemy dwie proste metody: jedną pozwalającą na dodawanie nowych wpisów do kalendarza oraz drugą, która pozwala na modyfikację atrybutów już istniejących.
Aktualizacje sklepu Redux wymagają dwóch części. Z jednej strony mamy akcje sygnalizujące zmianę, którą chcemy wprowadzić, a z drugiej reduktor, który biorąc pod uwagę aktualny stan naszego sklepu i akcję zlecającą aktualizację, wprowadza niezbędne zmiany do obecnego stanu. wygenerować nowy stan.
Biorąc to pod uwagę, oto działania, które możemy mieć w naszym sklepie:
function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }a oto reduktor:
function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }Nie spiesz się, aby to wszystko zrozumieć i ruszajmy dalej!
Od JavaScript do TypeScript
Pierwszą rzeczą, którą powinniśmy zrobić, to przetłumaczyć poprzedni kod na TypeScript. Cóż, ponieważ TypeScript jest nadzbiorem JavaScript, już nim jest… ale jeśli skopiujesz i wkleisz poprzednie funkcje do TypeScript Playground, zobaczysz, że kompilator sporo narzeka, ponieważ istnieje zbyt wiele zmiennych, których typ niejawny to any . Więc naprawmy to najpierw, jawnie dodając kilka podstawowych typów.
Wszystko, co musimy zrobić, to jawnie dodać any typ do wszystkiego, co jest „złożone” (takie jak stan naszej aplikacji) i użyć number lub string lub czegokolwiek, co chcemy, do dowolnej innej zmiennej/argumentu. Na przykład oryginalny selektor JavaScript:
function getPost( state, id ) { return state.posts[ id ]; }z jawnymi typami TypeScript wyglądałoby tak:
function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; } Jak widać, prosta czynność polegająca na wpisaniu naszego kodu (nawet gdy używamy „typów ogólnych”), oferuje wiele informacji na pierwszy rzut oka; wyraźna poprawa w porównaniu do podstawowego JavaScript! W tym przypadku, na przykład, widzimy, że getPost oczekuje number (identyfikator postu to liczba całkowita, pamiętasz?), a wynikiem będzie albo coś, jeśli post istnieje ( any ), albo nic, jeśli nie ( undefined ).
Tutaj masz link z całym typem kodu używającym prostych typów, aby kompilator nie narzekał.

Twórz i używaj niestandardowych typów danych w TypeScript
Teraz, gdy kompilator jest zadowolony z naszego kodu źródłowego, czas zastanowić się, jak możemy go ulepszyć. W tym celu zawsze proponuję zacząć od modelowania koncepcji, które mamy w naszej domenie.
Tworzenie niestandardowego typu postów
Wiemy, że nasz sklep będzie zawierał głównie posty, więc argumentuję, że pierwszym krokiem jest wymodelowanie, czym jest post i jakie informacje o nim posiadamy. Widzieliśmy już, jak tworzyć niestandardowe typy w zeszłym tygodniu, więc spróbujmy dzisiaj z koncepcją posta:
type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; }; Żadnych niespodzianek, prawda? Post to obiekt, który ma kilka atrybutów, np. id liczbowy , title tekstowy i tak dalej.
Inną ważną informacją, jaką ma każdy sklep Redux, jest, jak się domyślacie, jego stan. W poprzednim rozdziale omówiliśmy już jakie posiada atrybuty, więc zdefiniujmy podstawowy kształt naszego typu State :
type State = { posts: any; days: any; }; Poprawa typu State
Teraz wiemy, że State ma dwa atrybuty ( posts i days ), ale nie wiemy zbyt wiele o tym, czym one są, ponieważ mogą to być any rzeczy. Powiedzieliśmy, że chcemy, aby oba atrybuty były słownikami. To znaczy, biorąc pod uwagę określone zapytanie (albo identyfikator postu dla posts lub datę dla days ), potrzebujemy powiązanych danych (odpowiednio posta lub listy postów). Wiemy, że możemy zaimplementować słownik za pomocą obiektu, ale jak mamy reprezentować słownik w TypeScript?
Jeśli spojrzymy na dokumentację TypeScript, zobaczymy, że zawiera ona kilka typów narzędzi do radzenia sobie z dość powszechnymi sytuacjami. W szczególności istnieje typ o nazwie Record , który wydaje się być tym, którego potrzebujemy: pozwala nam wpisać zmienną za pomocą par klucz/wartość, w których klucz ma określony typ Keys , a wartości są typu Type . Jeśli zastosujemy ten typ do naszego przykładu, otrzymalibyśmy coś takiego:
type State = { posts: Record<number, Post>; days: Record<string, number[]>; }; Z perspektywy kompilatora typ Record działa w ten sposób, że przy danej wartości Keys (w naszym przykładzie number dla posts i string dla days ) jego wynikiem będzie zawsze obiekt typu Type (w naszym przypadku Odpowiednio Post lub number[] ). Problem polega na tym, że nie tak chcemy, aby nasz słownik zachowywał się: kiedy szukamy konkretnego posta przy użyciu jego identyfikatora, chcemy, aby kompilator wiedział, że możemy znaleźć powiązany post lub nie, co oznacza, że wynikiem może być Post lub undefined .
Na szczęście możemy to łatwo naprawić, używając jeszcze innego typu narzędzia, typu Partial :
type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };Ulepszanie naszego kodu za pomocą aliasów typów
Spójrz na atrybut posts w naszym stanie… Co widzisz? Słownik, który indeksuje posty typu Post z numerami, prawda? Teraz wyobraź sobie, jak przeglądasz ten kod w pracy. Jeśli napotkasz taki typ, możesz założyć, że number indeksująca posty jest prawdopodobnie identyfikatorem indeksowanych postów… ale to tylko założenie; musiałbyś przejrzeć kod, aby się upewnić. A co z days ? „Losowe ciągi indeksujące listy liczb”. To nie jest zbyt pomocne, prawda?
Typy TypeScript pomagają nam pisać bardziej niezawodny kod dzięki sprawdzeniu kompilatora, ale oferują znacznie więcej. Jeśli użyjesz znaczących typów, Twój kod będzie lepiej udokumentowany i łatwiejszy w utrzymaniu. Więc aliasujmy istniejące typy, aby stworzyć znaczące typy, dobrze?
Na przykład wiedząc, że identyfikatory postów ( number ) i daty ( string ) są istotne dla naszej domeny, możemy łatwo utworzyć następujące aliasy typów:
type PostId = number; type Day = string;a następnie przepisz nasze oryginalne typy, używając tych aliasów:
type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; }; Innym typem aliasu, którego możemy użyć do poprawy czytelności naszego kodu, jest typ Dictionary , który „ukrywa” złożoność używania Partial i Record za wygodną strukturą:
type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;ulepszenie naszego kodu źródłowego:
type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; I to wszystko! Masz to! Dzięki zaledwie trzem prostym aliasom typów byliśmy w stanie udokumentować kod w sposób, który jest wyraźnie lepszy niż używanie komentarzy. Każdy programista, który przyjdzie po nas, będzie wiedział na pierwszy rzut oka, że posts to słownik, który indeksuje obiekty typu Post przy użyciu ich PostId , a days to struktura danych, która przy danym Day zwraca identyfikatory postów z listy. To całkiem niesamowite, jeśli mnie zapytasz.
Ale nie tylko same definicje typów są lepsze… jeśli użyjemy tych nowych typów w całym naszym kodzie:
function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }korzysta również z tej nowej warstwy semantycznej! Możesz zobaczyć nową wersję naszego wpisanego kodu tutaj.
A tak przy okazji, pamiętaj, że aliasy typów są z punktu widzenia kompilatora nie do odróżnienia od „oryginalnego” typu. Oznacza to, że np. PostId i number są całkowicie zamienne. Więc nie oczekuj, że kompilator wyzwoli błąd, jeśli przypiszesz PostId do number lub odwrotnie (jak widać w tym małym przykładzie); służą po prostu do dodawania semantyki do naszego kodu źródłowego.
Następne kroki
Jak widać, możesz wpisywać kod JavaScript za pomocą typów TypeScript przyrostowo, a tym samym poprawia się jego jakość i czytelność. W dzisiejszym poście widzieliśmy szczegółowo przykład rzeczywistej implementacji aplikacji React + Redux i widzieliśmy, jak można ją ulepszyć przy stosunkowo niewielkim wysiłku. Ale przed nami jeszcze długa droga.
W następnym poście wpiszemy wszystkie pozostałe zmienne/argumenty, które aktualnie używają any typu, a także poznamy kilka zaawansowanych wyczynów TypeScript. Mam nadzieję, że spodobała Ci się ta pierwsza część, a jeśli tak, podziel się nią ze znajomymi i współpracownikami.
Polecane zdjęcie autorstwa Danielle MacInnes na Unsplash.
