Hinzufügen von TypeScript zu @wordpress/data Stores

Veröffentlicht: 2021-02-05

Letztes Jahr haben wir viel über TypeScript gesprochen. In einem meiner letzten Beiträge haben wir anhand eines realen Beispiels gesehen, wie Sie TypeScript in Ihren WordPress-Plugins verwenden und insbesondere, wie Sie einen Redux -Speicher verbessern können, indem Sie Typen zu unseren Selektoren, Aktionen und Reduzierern hinzufügen.

In diesem Beispiel sind wir von einfachem JavaScript-Code wie folgt ausgegangen:

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

wobei das einzige, was uns Hinweise darauf gab, was jede Funktion tut und was jeder Parameter ist, von unseren Benennungsfähigkeiten abhängt, bis hin zum folgenden verbesserten TypeScript-Gegenstück:

 // 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 { … }

was alles viel klarer macht, da alles richtig geschrieben ist:

 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; };

Vor ein paar Wochen habe ich an unserem neuen Plugin Nelio Unlocker gearbeitet und bin bei der Anwendung all dieser Techniken auf ein Problem gestoßen. Lassen Sie uns also das Problem überprüfen und lernen, wie man es überwindet!

Das Problem

Wie Sie vielleicht bereits wissen, greifen wir, wenn wir die in unserem Shop definierten Selektoren und/oder Aktionen verwenden möchten, über React-Hooks (mit useSelect und useDispatch ) oder über Komponenten höherer Ordnung (mit withSelect und withDispatch ) darauf zu. , die alle vom Paket @wordpress/data bereitgestellt werden.

Wenn wir beispielsweise den Selektor getPost und die gerade gesehene Aktion updatePost verwenden möchten, müssen wir nur so etwas tun (vorausgesetzt, unser Geschäft heißt nelio-store ):

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

Im vorherigen Snippet können Sie sehen, dass wir mit React-Hooks auf unsere Selektoren und Aktionen zugreifen. Aber woher zum Teufel weiß TypeScript, dass diese Selektoren und Aktionen existieren, geschweige denn, was seine Typen sind?

Nun, das ist genau das Problem, mit dem ich konfrontiert war. Das heißt, ich wollte wissen, wie ich TypeScript mitteilen kann, dass das Ergebnis des Zugriffs auf select('nelio-store') ein Objekt ist, das alle unsere Store-Selektoren enthält, und dispatch('nelio-store') ein Objekt mit unseren Store-Aktionen ist .

Die Lösung

In unserem letzten Beitrag zu TypeScript haben wir über polymorphe Funktionen gesprochen. Polymorphe Funktionen ermöglichen es uns, basierend auf den angegebenen Argumenten verschiedene Rückgabetypen anzugeben. Nun, mit TypeScript-Polymorphismus können wir festlegen, dass wir beim Aufrufen der select oder dispatch -Methoden des @wordpress/data Pakets mit dem Namen unseres Stores als Parameter als Ergebnis unsere Selektoren bzw. unsere Aktionen erhalten.

Fügen Sie dazu einfach einen declare module in die Datei ein, in der wir unseren Shop wie folgt registrieren:

 // 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; }

und definieren Sie dann, was die Typen von Selectors und Actions tatsächlich sind:

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

So weit, so gut, oder? Das einzige „Problem“ ist, dass wir die Typen von Selectors und Actions manuell definieren müssen, was seltsam klingt, da TypeScript bereits weiß, dass wir eine Reihe von richtig typisierten selectors und actions haben …

Bearbeiten von Funktionstypen in TypeScript

Wenn wir uns die Typen der von uns importierten actions und selectors ansehen, werden wir feststellen, dass TypeScript uns Folgendes mitteilt:

 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; }

Wie Sie sehen können, sind ihre Typen eine exakte Kopie der Typen, die wir im vorherigen Abschnitt manuell definiert haben. Nun, fast genau: Selektoren fehlt ihr erstes Argument (der store state , weil er nicht vorhanden ist, wenn wir einen Selektor von select aufrufen) und Aktionen geben void zurück (da Aktionen, die über dispatch aufgerufen werden, nichts zurückgeben).

Können wir sie verwenden, um die benötigten Selectors und Actions automatisch zu generieren?

So entfernen Sie den ersten Parameter eines Funktionstyps in TypeScript

Konzentrieren wir uns für einen Moment auf den getPost Selektor. Sein Typ ist wie folgt:

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

Wie wir gerade gesagt haben, brauchen wir einen neuen Funktionstyp, der den state nicht hat:

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

Wir brauchen also TypeScript, um einen neuen Typ aus einem bereits vorhandenen Typ zu generieren. Dies kann durch die Kombination mehrerer fortgeschrittener Funktionalitäten der Sprache erreicht werden:

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

Kompliziert, oder? Schauen wir uns genauer an, was hier los ist:

  • type OmitFirstArg<F> . Zunächst definieren wir einen neuen generischen Hilfstyp ( OmitFirstArg ). Im Allgemeinen ist ein generischer Typ ein Typ, mit dem wir neue Typen aus bereits vorhandenen Typen definieren können. Zum Beispiel sind Sie wahrscheinlich mit dem Typ Array<T> vertraut, da Sie damit Listen von Dingen erstellen können: Array<string> ist eine Liste von Strings, Array<Post> ist eine Liste von Post usw. Nun, im Folgenden Dieser Begriff, OmitFirstArg<F> ist ein Hilfstyp, der das erste Argument einer Funktion entfernt.
  • Da es sich um einen generischen Typ handelt, könnten wir ihn theoretisch mit jedem anderen TypeScript-Typ verwenden. Das heißt, Dinge wie OmitFirstArg<string> und OmitFirstArg<Post> sind möglich … obwohl wir wissen, dass dieser Typ nur mit Funktionen verwendet werden sollte, die mindestens ein Argument haben. Um sicherzustellen, dass dieser Hilfstyp nur mit Funktionen verwendet wird, definieren wir ihn als bedingten Typ. Der bedingte Typ lässt uns spezifizieren, was der resultierende Typ basierend auf einer Bedingung sein soll: „Wenn F eine Funktion mit mindestens einem Argument (Bedingung) ist, ist der resultierende Typ eine andere Funktion, bei der das erste Argument entfernt wurde (Typ, wenn Bedingung ist wahr); Verwenden Sie andernfalls den Typ never (Typ, wenn die Bedingung falsch ist).“
  • F extends XXX . Dies ist die Formel zur Angabe der Bedingung. Wollen Sie überprüfen, ob F eine Zeichenfolge ist? Geben Sie einfach ein: F extends string . Kinderleicht. Aber was ist mit „einer Funktion mit einem Argument“? Das klingt natürlich komplizierter…
  • (x: any, ...args: infer P) => infer R . Dies ist ein Funktionstyp: Wir beginnen mit den Argumenten (in Klammern), gefolgt von einem Pfeil, gefolgt vom Rückgabetyp der Funktion. In diesem speziellen Fall verlangen wir, dass die Funktion ein Argument x hat (dessen spezifischer Typ irrelevant ist). Diese Typdefinition hat zwei interessante Bits. Einerseits verwenden wir den Restoperator, um die Typen P der verbleibenden args (falls vorhanden) zu erfassen. Andererseits verwenden wir die Typinferenz ( infer ) von TypeScript, um zu wissen, was diese Typen P wirklich sind, sowie den genauen Rückgabetyp R .
  • ? (...args: P) => R : never ? (...args: P) => R : never . Schließlich vervollständigen wir den bedingten Typ. Wenn F eine Funktion war, ist der Rückgabetyp eine neue Funktion, deren Argumente vom Typ P sind und deren Rückgabetyp R ist. Wenn dies nicht der Fall ist, ist der Rückgabetyp never .

So können wir diesen Hilfstyp verwenden, um den gewünschten neuen Typ zu erstellen:

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

und wir sind dem, was wir wollen, bereits einen Schritt näher gekommen! Hier sehen Sie dieses Beispiel im Playground.

So ändern Sie den Rückgabetyp eines Funktionstyps in TypeScript

Ich bin sicher, Sie kennen die Antwort bereits von know: Wir brauchen einen generischen Hilfstyp, der einen Funktionstyp akzeptiert und einen neuen Funktionstyp zurückgibt. Etwas wie das:

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

Einfach richtig? Es ist ziemlich ähnlich zu dem, was wir im vorherigen Abschnitt gemacht haben: Wir erfassen die Typen der args in P (es ist diesmal nicht erforderlich, mindestens ein Argument x zu benötigen) und ignorieren den Rückgabetyp. Wenn F eine Funktion ist, geben Sie eine neue Funktion zurück, die void zurückgibt. Andernfalls never zurückgeben. Fantastisch!

Sehen Sie sich das auf dem Spielplatz an.

So ordnen Sie einen Objekttyp einem anderen Objekttyp in TypeScript zu

Unsere Aktionen und unsere Selektoren sind zwei Objekte, deren Schlüssel die Namen dieser Aktionen und Selektoren sind und deren Werte die Funktionen selbst sind. Das bedeutet, dass die Typen dieser Objekte wie folgt aussehen:

 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; }

In den beiden vorangegangenen Abschnitten haben wir gelernt, wie man einen Funktionstyp in einen anderen Funktionstyp umwandelt. Das bedeutet, dass wir neue Typen wie folgt von Hand definieren könnten:

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

Aber das ist natürlich nicht nachhaltig: Wir geben die Namen der Funktionen in beiden Typen manuell an. Natürlich möchten wir die ursprünglichen Typdefinitionen von actions und selectors automatisch neuen Typen zuordnen.

So können Sie das in TypeScript tun:

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

Hoffentlich macht das schon Sinn, aber lassen Sie uns schnell abspulen, was das vorherige Snippet sowieso tut:

  • type OmitFirstArgs<O> . Wir erstellen einen neuen generischen Hilfstyp, der ein Objekt O .
  • Das Ergebnis ist ein anderer Objekttyp (wie die geschweiften Klammern verraten {...} ).
  • [K in keyof O] . Wir kennen die genauen Schlüssel des neuen Objekts nicht, aber wir wissen, dass es die gleichen Schlüssel sein müssen wie die in O enthaltenen. Das sagen wir also TypeScript: Wir wollen alle Schlüssel K , die ein keyof O sind.
  • Und dann ist der Typ für jeden Schlüssel K OmitFirstArg<O[K]> . Das heißt, wir erhalten den ursprünglichen Typ ( O[K] ) und wandeln ihn mithilfe des von uns definierten Hilfstyps (in diesem Fall OmitFirstArg ) in den gewünschten Typ um.
  • Schließlich machen wir dasselbe mit RemoveReturnTypes und dem ursprünglichen RemoveReturnType .

Erweitern @wordpress/data mit unseren Selektoren und Aktionen

Wenn Sie die vier Hilfstypen, die wir heute gesehen haben, in einer global.d.ts -Datei hinzufügen und im Stammverzeichnis Ihres Projekts speichern, können Sie endlich alles kombinieren, was wir in diesem Beitrag gesehen haben, um das ursprüngliche Problem zu lösen:

 // 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; }

Und das ist es! Ich hoffe, Ihnen hat dieser Entwicklertipp gefallen, und wenn ja, teilen Sie ihn bitte mit Ihren Kollegen und Freunden. Oh! Und wenn Sie einen anderen Ansatz kennen, um das gleiche Ergebnis zu erzielen, teilen Sie es mir in den Kommentaren mit.

Vorgestelltes Bild von Gabriel Crismariu auf Unsplash.