Adăugarea TypeScript la @wordpress/data Stores
Publicat: 2021-02-05Anul trecut am vorbit mult despre TypeScript. Într-una dintre cele mai recente postări ale mele, am văzut cum să folosim TypeScript în plugin-urile WordPress printr-un exemplu real și, în special, cum să îmbunătățim un magazin Redux adăugând tipuri la selectoarele, acțiunile și reductoarele noastre.
În exemplul menționat, am trecut de la codul JavaScript de bază, astfel:
// Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }unde singurul lucru care ne-a dat indicii despre ce face fiecare funcție și despre ce este fiecare parametru depinde de abilitățile noastre de denumire, la următorul omologat TypeScript îmbunătățit:
// 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 { … }ceea ce face totul mult mai clar, deoarece totul este scris corect:
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; };Acum câteva săptămâni, lucram la noul nostru plugin, Nelio Unlocker, și am întâmpinat o problemă când am aplicat toate aceste tehnici. Deci, haideți să revizuim problema menționată și să învățăm cum să o depășim!
Problema
După cum poate știți deja, atunci când dorim să folosim selectoarele și/sau acțiunile pe care le-am definit în magazinul nostru, o facem accesând-le prin intermediul cârligelor React (cu useSelect și useDispatch ) sau prin componente de ordin superior (cu withSelect și withDispatch ) , toate acestea fiind furnizate de pachetul @wordpress/data .
De exemplu, dacă dorim să folosim selectorul getPost și acțiunea updatePost pe care tocmai am văzut-o, tot ce trebuie să facem este ceva de genul acesta (presupunând că magazinul nostru se numește nelio-store ):
const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };În fragmentul anterior, puteți vedea că ne accesăm selectoarele și acțiunile folosind cârligele React. Dar de unde dracu’ știe TypeScript că acești selectori și acțiuni există, să nu mai vorbim de tipurile lor?
Ei bine, exact asta e problema cu care m-am confruntat. Adică, am vrut să știu cum aș putea spune TypeScript că rezultatul accesării select('nelio-store') este un obiect care conține toți selectoarele noastre de magazine și dispatch('nelio-store') este un obiect cu acțiunile noastre din magazin. .
Soluția
În ultima noastră postare despre TypeScript am vorbit despre funcțiile polimorfe. Funcțiile polimorfe ne permit să specificăm diferite tipuri de returnări pe baza argumentelor date. Ei bine, folosind polimorfismul TypeScript putem specifica că, atunci când apelăm metodele select sau dispatch ale pachetului @wordpress/data cu numele magazinului nostru ca parametru, rezultatul pe care îl obținem sunt selectoarele noastre și respectiv acțiunile noastre.
Pentru a face acest lucru, pur și simplu adăugați un bloc de declare module în fișierul în care ne înregistrăm magazinul, după cum urmează:
// 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; } și apoi definiți ce tipuri de Selectors și Actions sunt de fapt:
type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; } Până acum, e bine, nu? Singura „problemă” este că trebuie să definim manual tipurile de Selectors și Actions , ceea ce sună ciudat, având în vedere că TypeScript știe deja că avem un set de selectors și actions tastate corespunzător...
Manipularea tipurilor de funcții în TypeScript
Dacă ne uităm la tipurile de actions și obiecte selectors pe care le-am importat, vom vedea că TypeScript ne spune următoarele:
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; } După cum puteți vedea, tipurile lor sunt o copie exactă a tipurilor pe care le-am definit manual în secțiunea anterioară. Ei bine, aproape exact: selectorilor le lipsește primul argument ( state magazinului , deoarece nu este prezent atunci când apelăm un selector din select ) și acțiunile returnează void (întrucât acțiunile numite prin dispatch nu returnează nimic).
Le putem folosi pentru a genera automat tipurile de Selectors și Actions de care avem nevoie?
Cum să eliminați primul parametru al unui tip de funcție în TypeScript
Să ne concentrăm pentru un moment pe selectorul getPost . Tipul său este următorul:
// Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined După cum tocmai am spus, avem nevoie de un nou tip de funcție care nu are parametrul de state :
// New type ( id: PostId ) => Post | undefinedDeci, avem nevoie de TypeScript pentru a genera un nou tip dintr-un tip deja existent. Acest lucru poate fi realizat prin combinarea mai multor funcționalități avansate ale limbajului:
type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;Complicat, nu? Să aruncăm o privire mai atentă la ce se întâmplă aici:
-
type OmitFirstArg<F>. În primul rând, definim un nou tip generic auxiliar (OmitFirstArg). În general, un tip generic este un tip care ne permite să definim noi tipuri din tipurile deja existente. De exemplu, probabil că sunteți familiarizat cu tipulArray<T>, deoarece vă permite să creați liste de lucruri:Array<string>este o listă de șiruri,Array<Post>este o listă dePost, etc. Ei bine, următoarele această noțiune,OmitFirstArg<F>este un tip de ajutor care elimină primul argument al unei funcții. - Deoarece acesta este un tip generic, teoretic l-am putea folosi cu orice alt tip TypeScript. Adică, lucruri precum
OmitFirstArg<string>șiOmitFirstArg<Post>sunt posibile... chiar dacă știm că acest tip ar trebui folosit doar cu funcții care au cel puțin un argument. Pentru a ne asigura că acest tip de ajutor este folosit doar cu funcții, îl vom defini ca tip condiționat. Tipul condițional ne permite să specificăm ce tip rezultat ar trebui să fie bazat pe o condiție: „dacăFeste o funcție cu cel puțin un argument (condiție), tipul rezultat este o altă funcție în care primul argument a fost eliminat (tip când condiția este Adevărat); în caz contrar, utilizați tipulnever(tip când condiția este falsă).” -
F extends XXX. Aceasta este formula pentru a specifica condiția. Vrei să verifici dacăFeste un șir? Doar tastați:F extends string. Ușor de gălăgie. Dar cum rămâne cu „o funcție cu un singur argument?” Cu siguranță sună mai complicat... -
(x: any, ...args: infer P) => infer R. Acesta este un tip de funcție: începem cu argumentele (în paranteză), urmate de o săgeată, urmată de tipul de returnare al funcției. În acest caz particular, solicităm ca funcția să aibă un argumentx(al cărui tip specific este irelevant). Această definiție a tipului are două părți interesante. Pe de o parte, folosim operatorul rest pentru a captura tipurilePaleargsrămase (dacă există). Pe de altă parte, folosim inferența de tip (infer) a lui TypeScript pentru a ști ce sunt cu adevărat acele tipuriP, precum și tipul exact de returnareR. -
? (...args: P) => R : never? (...args: P) => R : never. În cele din urmă, completăm tipul condiționat. DacăFa fost o funcție, tipul returnat este o funcție nouă ale cărei argumente sunt de tipPși al cărei tip returnat esteR. Dacă nu este, tipul de returnare nu estenever.
Iată cum putem folosi acest tip de ajutor pentru a crea noul tip dorit:

const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;și suntem deja cu un pas mai aproape de a realiza ceea ce ne dorim! Aici puteți vedea acest exemplu în locul de joacă.
Cum se schimbă tipul de returnare al unui tip de funcție în TypeScript
Sunt sigur că știți deja răspunsul prin know: avem nevoie de un tip generic auxiliar care preia un tip de funcție și returnează un nou tip de funcție. Ceva de genul:
type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never; Ușor, nu? Este destul de asemănător cu ceea ce am făcut în secțiunea anterioară: captăm tipurile de args în P (nu este nevoie să cerem cel puțin un argument x de data aceasta) și ignorăm tipul returnat. Dacă F este o funcție, returnează o funcție nouă care returnează void . În caz contrar, nu te întoarce never . Minunat!
Verificați asta în locul de joacă.
Cum să mapați un tip de obiect cu un alt tip de obiect în TypeScript
Acțiunile noastre și selectorii noștri sunt două obiecte ale căror chei sunt numele acelor acțiuni și selectori și ale căror valori sunt funcțiile în sine. Aceasta înseamnă că tipurile acestor obiecte arată astfel:
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; }În cele două secțiuni anterioare am învățat cum să transformăm un tip de funcție într-un alt tip de funcție. Aceasta înseamnă că am putea defini noi tipuri manual, astfel:
type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; }; Dar, desigur, acest lucru nu este sustenabil în timp: specificăm manual numele funcțiilor în ambele tipuri. În mod clar, dorim să mapăm automat definițiile tipului inițial ale actions și ale selectors la tipuri noi.
Iată cum puteți face asta în TypeScript:
type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }Sperăm că acest lucru are deja sens, dar oricum să dezvăluim rapid ce face fragmentul anterior:
-
type OmitFirstArgs<O>. Creăm un nou tip generic auxiliar care preia un obiectO. - Rezultatul este un alt tip de obiect (după cum arată acoladele
{...}). -
[K in keyof O]. Nu știm cheile exacte pe care le va avea noul obiect, dar știm că trebuie să fie aceleași chei cu cele incluse înO. Deci asta este ceea ce îi spunem TypeScript: vrem toate tasteleKcare sunt okeyof O - Și apoi, pentru fiecare cheie
K, tipul acesteia esteOmitFirstArg<O[K]>. Adică obținem tipul original (O[K]) și îl transformăm în tipul dorit folosind tipul auxiliar pe care l-am definit (în acest caz,OmitFirstArg). - În cele din urmă, facem același lucru cu
RemoveReturnTypesși tipul auxiliar originalRemoveReturnType.
Extinderea @wordpress/data cu selectoarele și acțiunile noastre
Dacă adăugați cele patru tipuri auxiliare pe care le-am văzut astăzi într-un fișier global.d.ts și îl salvați în rădăcina proiectului dvs., puteți, în sfârșit, să combinați tot ce am văzut în această postare pentru a rezolva problema inițială:
// 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; }Si asta e! Sper că v-a plăcut acest sfat pentru dezvoltatori și, dacă v-a plăcut, vă rugăm să-l împărtășiți colegilor și prietenilor dvs. Oh! Și dacă cunoașteți o abordare diferită pentru a obține același rezultat, spuneți-mi în comentarii.
Imagine prezentată de Gabriel Crismariu pe Unsplash.
