Dodawanie TypeScriptu do @wordpress/data Stores
Opublikowany: 2021-02-05W 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 | undefinedDlatego 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 typArray<T>, ponieważ pozwala on tworzyć listy rzeczy:Array<string>to lista ciągów,Array<Post>to listaPostitp. 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>iOmitFirstArg<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śliFjest 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 typunever(wpisz, gdy warunek jest fałszywy).” -
F extends XXX. To jest formuła określająca warunek. Czy chcesz sprawdzić, czyFjest 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 argumentx(którego konkretny typ jest nieistotny). Ta definicja typu ma dwa interesujące bity. Z jednej strony używamy operatora reszty do uchwycenia typówPpozostałychargs(jeśli istnieją). Z drugiej strony, używamy wnioskowania o typie TypeScript (infer), aby dowiedzieć się, czym naprawdę są te typyP, a także jaki jest dokładny typ zwracanyR. -
? (...args: P) => R : never? (...args: P) => R : never. Na koniec uzupełniamy typ warunkowy. JeśliFbyła funkcją, typem zwracanym jest nowa funkcja, której argumenty są typuP, a typem zwracanym jestR. Jeśli tak nie jest, typem zwracanym jestnever.
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 obiektO. - 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 wO. Oto, co mówimy TypeScript: chcemy, aby wszystkie kluczeK, które sąkeyof O. - A następnie dla każdego klucza
Kjego typ toOmitFirstArg<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 przypadkuOmitFirstArg). - Na koniec robimy to samo z
RemoveReturnTypesi pierwotnym typem pomocniczymRemoveReturnType.
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.
