Dodawanie TypeScriptu do @wordpress/data Stores

Opublikowany: 2021-02-05

W zeszłym roku dużo rozmawialiśmy o TypeScript. W jednym z moich ostatnich postów widzieliśmy, jak używać TypeScriptu we wtyczkach do WordPressa na prawdziwym przykładzie, a w szczególności, jak ulepszyć sklep Redux, dodając typy do naszych selektorów, akcji i reduktorów.

We wspomnianym przykładzie przeszliśmy od podstawowego kodu JavaScript w następujący sposób:

 // Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }

gdzie jedyną rzeczą, która dała nam wskazówki na temat tego, co robi każda funkcja i jaki jest każdy parametr, zależy od naszych umiejętności nazewnictwa, do następującego ulepszonego odpowiednika TypeScript:

 // Selectors function getPost( state: State, id: PostId ): Post | undefined { … } function getPostsInDay( state: State, day: Day ): PostId[] { … } // Actions function receiveNewPost( post: Post ): ReceiveNewPostAction { … } function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction { … } // Reducer function reducer( state: State, action: Action ): State { … }

co sprawia, że ​​wszystko jest znacznie jaśniejsze, ponieważ wszystko jest poprawnie napisane:

 type PostId = number; type Day = string; type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Dictionary; days: Dictionary; };

Kilka tygodni temu pracowałem nad naszą nową wtyczką, Nelio Unlocker, i napotkałem problem podczas stosowania wszystkich tych technik. Przyjrzyjmy się więc wspomnianemu problemowi i dowiedzmy się, jak go rozwiązać!

Problem

Jak być może już wiesz, kiedy chcemy skorzystać z selektorów i/lub akcji, które zdefiniowaliśmy w naszym sklepie, robimy to poprzez dostęp do nich poprzez hooki React (za pomocą useSelect i useDispatch ) lub poprzez komponenty wyższego rzędu (za pomocą withSelect i withDispatch ) , z których wszystkie są dostarczane przez pakiet @wordpress/data .

Na przykład, jeśli chcielibyśmy użyć selektora getPost i akcji updatePost , którą właśnie widzieliśmy, wystarczy, że zrobimy coś takiego (zakładając, że nasz sklep nazywa się nelio-store ):

 const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };

W poprzednim fragmencie możesz zobaczyć, że uzyskujemy dostęp do naszych selektorów i akcji za pomocą haków React. Ale skąd do cholery TypeScript wie, że te selektory i akcje istnieją, nie mówiąc już o ich typach?

Cóż, to jest dokładnie problem, z którym się spotkałem. To znaczy, chciałem wiedzieć, jak mogę powiedzieć TypeScriptowi, że wynikiem dostępu do select('nelio-store') jest obiekt, który zawiera wszystkie nasze selektory sklepu, a dispatch('nelio-store') jest obiektem z naszymi akcjami sklepu .

Rozwiązanie

W naszym ostatnim poście na temat TypeScript mówiliśmy o funkcjach polimorficznych. Funkcje polimorficzne pozwalają nam na określenie różnych typów zwracanych na podstawie podanych argumentów. Cóż, używając polimorfizmu TypeScript możemy określić, że kiedy wywołujemy metody select lub dispatch pakietu @wordpress/data z nazwą naszego sklepu jako parametrem, otrzymujemy odpowiednio nasze selektory i nasze akcje.

Aby to zrobić wystarczy dodać blok declare module w pliku, w którym rejestrujemy nasz sklep w następujący sposób:

 // WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; }

a następnie zdefiniuj, czym właściwie są Selectors i Actions :

 type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; }

Jak dotąd, tak dobrze, prawda? Jedynym „problemem” jest to, że musimy ręcznie zdefiniować typy Selectors i Actions , co brzmi dziwnie, biorąc pod uwagę, że TypeScript już wie, że mamy zestaw poprawnie wpisanych selectors i actions

Manipulowanie typami funkcji w TypeScript

Jeśli przyjrzymy się typom obiektów actions i selectors , które zaimportowaliśmy, zobaczymy, że TypeScript mówi nam co następuje:

 typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; }

Jak widać, ich typy są dokładną kopią typów, które ręcznie zdefiniowaliśmy w poprzedniej sekcji. Cóż, prawie dokładnie: selektorom brakuje pierwszego argumentu ( state sklepu , ponieważ nie ma go, gdy wywołujemy selektor z select ), a akcje zwracają void (ponieważ akcje wywoływane przez dispatch nic nie zwracają).

Czy możemy ich użyć do automatycznego generowania potrzebnych typów Selectors i Actions ?

Jak usunąć pierwszy parametr typu funkcji w TypeScript?

Skupmy się na chwilę na selektorze getPost . Jego typ jest następujący:

 // Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined

Jak już powiedzieliśmy, potrzebujemy nowego typu funkcji, która nie ma parametru state :

 // New type ( id: PostId ) => Post | undefined

Dlatego potrzebujemy TypeScript, aby wygenerować nowy typ z już istniejącego typu. Można to osiągnąć poprzez połączenie kilku zaawansowanych funkcjonalności języka:

 type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;

Skomplikowane, co? Przyjrzyjmy się bliżej temu, co się tutaj dzieje:

  • type OmitFirstArg<F> . Przede wszystkim definiujemy nowy pomocniczy typ ogólny ( OmitFirstArg ) . Ogólnie rzecz biorąc, typ ogólny to typ, który pozwala nam zdefiniować nowe typy z już istniejących typów. Na przykład, prawdopodobnie znasz typ Array<T> , ponieważ pozwala on tworzyć listy rzeczy: Array<string> to lista ciągów, Array<Post> to lista Post itp. Cóż, po to pojęcie, OmitFirstArg<F> jest typem pomocnika, który usuwa pierwszy argument funkcji.
  • Ponieważ jest to typ ogólny, teoretycznie moglibyśmy użyć go z dowolnym innym typem TypeScript. Oznacza to, że rzeczy takie jak OmitFirstArg<string> i OmitFirstArg<Post> są możliwe… chociaż wiemy, że ten typ powinien być używany tylko z funkcjami, które mają co najmniej jeden argument. Aby upewnić się, że ten typ pomocnika jest używany tylko z funkcjami, zdefiniujemy go jako typ warunkowy. Typ warunkowy określmy, jaki typ wynikowy powinien być oparty na warunku: „jeśli F jest funkcją z co najmniej jednym argumentem (warunkiem), typem wynikowym jest inna funkcja, w której pierwszy argument został usunięty (typ, gdy warunek jest prawda); w przeciwnym razie użyj typu never (wpisz, gdy warunek jest fałszywy).”
  • F extends XXX . To jest formuła określająca warunek. Czy chcesz sprawdzić, czy F jest ciągiem? Wystarczy wpisać: F extends string . Bułka z masłem. Ale co z „funkcją z jednym argumentem?” To z pewnością brzmi bardziej skomplikowanie…
  • (x: any, ...args: infer P) => infer R . To jest typ funkcji: zaczynamy od argumentów (w nawiasach), po których następuje strzałka, po której następuje typ zwracany funkcji. W tym konkretnym przypadku wymagamy, aby funkcja miała jeden argument x (którego konkretny typ jest nieistotny). Ta definicja typu ma dwa interesujące bity. Z jednej strony używamy operatora reszty do uchwycenia typów P pozostałych args (jeśli istnieją). Z drugiej strony, używamy wnioskowania o typie TypeScript ( infer ), aby dowiedzieć się, czym naprawdę są te typy P , a także jaki jest dokładny typ zwracany R .
  • ? (...args: P) => R : never ? (...args: P) => R : never . Na koniec uzupełniamy typ warunkowy. Jeśli F była funkcją, typem zwracanym jest nowa funkcja, której argumenty są typu P , a typem zwracanym jest R . Jeśli tak nie jest, typem zwracanym jest never .

W ten sposób możemy użyć tego typu pomocnika do stworzenia nowego typu, który chcieliśmy:

 const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;

i jesteśmy już o krok bliżej do osiągnięcia tego, czego chcemy! Tutaj możesz zobaczyć ten przykład na placu zabaw.

Jak zmienić zwracany typ typu funkcji w TypeScript?

Jestem pewien, że znasz już odpowiedź przez wiem: potrzebujemy pomocniczego typu ogólnego, który przyjmuje typ funkcji i zwraca nowy typ funkcji. Coś takiego:

 type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never;

Łatwe, prawda? Jest to bardzo podobne do tego, co zrobiliśmy w poprzedniej sekcji: przechwytujemy typy args w P (nie ma potrzeby wymagać co najmniej jednego argumentu x tym razem) i ignorujemy typ zwracany. Jeśli F jest funkcją, zwróć nową funkcję, która zwraca void . W przeciwnym razie never wracaj . Świetny!

Sprawdź to na placu zabaw.

Jak zmapować typ obiektu na inny typ obiektu w TypeScript?

Nasze akcje i nasze selektory to dwa obiekty, których klucze są nazwami tych akcji i selektorów, a wartościami są same funkcje. Oznacza to, że typy tych obiektów wyglądają następująco:

 typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; }

W poprzednich dwóch rozdziałach nauczyliśmy się, jak przekształcić jeden typ funkcji w inny typ funkcji. Oznacza to, że moglibyśmy zdefiniować nowe typy ręcznie w następujący sposób:

 type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; };

Ale oczywiście nie jest to trwałe z upływem czasu: ręcznie określamy nazwy funkcji w obu typach. Oczywiście chcemy automatycznie mapować oryginalne definicje typów actions i selectors na nowe typy.

Oto jak możesz to zrobić w TypeScript:

 type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }

Mamy nadzieję, że to już ma sens, ale szybko rozwińmy, co i tak robi poprzedni fragment:

  • type OmitFirstArgs<O> . Tworzymy nowy pomocniczy typ ogólny, który przyjmuje obiekt O .
  • Wynikiem jest inny typ obiektu (ponieważ nawiasy klamrowe pokazują {...} ).
  • [K in keyof O] . Nie znamy dokładnych kluczy, które będzie miał nowy obiekt, ale wiemy, że muszą to być te same klucze, co te zawarte w O . Oto, co mówimy TypeScript: chcemy, aby wszystkie klucze K , które są keyof O .
  • A następnie dla każdego klucza K jego typ to OmitFirstArg<O[K]> . Oznacza to, że otrzymujemy oryginalny typ ( O[K] ) i przekształcamy go w żądany typ przy użyciu zdefiniowanego przez nas typu pomocniczego (w tym przypadku OmitFirstArg ).
  • Na koniec robimy to samo z RemoveReturnTypes i pierwotnym typem pomocniczym RemoveReturnType .

Rozszerzanie @wordpress/data o nasze selektory i akcje

Jeśli dodasz cztery typy pomocnicze, które widzieliśmy dzisiaj w pliku global.d.ts i zapiszesz je w katalogu głównym projektu, możesz w końcu połączyć wszystko, co widzieliśmy w tym poście, aby rozwiązać pierwotny problem:

 // WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; // Types type Selectors = OmitFirstArgs< typeof selectors >; type Actions = RemoveReturnTypes< typeof actions >; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; }

I to wszystko! Mam nadzieję, że spodobała Ci się ta wskazówka dla programistów, a jeśli tak, podziel się nią z kolegami i przyjaciółmi. Oh! A jeśli znasz inne podejście do uzyskania tego samego wyniku, powiedz mi w komentarzach.

Polecane zdjęcie Gabriela Crismariu na Unsplash.