Close up picture by Gabriel Crismariu of a Puzzle

Last year we talked a lot about TypeScript. In one of my latest posts we saw how to use TypeScript in your WordPress plugins through a real example and, in particular, how to improve a Redux store by adding types to our selectors, actions and reducers.

In said example, we went from basic JavaScript code like this:

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

where the only thing that gave us clues about what each function does and what each parameter is depends on our naming abilities, to the following improved TypeScript counterpart:

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

which makes everything much clearer, as everything is properly typed:

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

A couple of weeks ago I was working on our new plugin, Nelio Unlocker, and I ran into a problem when applying all these techniques. So let’s review said problem and learn how to overcome it!

The Problem

As you may already know, when we want to use the selectors and/or actions we defined in our store, we do so by accessing them through React hooks (with useSelect and useDispatch) or via higher-order components (with withSelect and withDispatch), all of which are provided by the @wordpress/data package.

For example, if we wanted to use the getPost selector and the updatePost action that we’ve just seen, all we have to do is something like this (assuming our store is named nelio-store):

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

In the previous snippet you can see we access our selectors and actions using React hooks. But how the heck does TypeScript know that those selectors and actions exist, let alone what its types are?

Well, that’s exactly the problem I faced. That is, I wanted to know how I could tell TypeScript that the result of accessing select('nelio-store') is an object that contains all our store selectors and dispatch('nelio-store') is an object with our store actions.

The Solution

In our last post on TypeScript we talked about polymorphic functions. Polymorphic functions let us to specify different return types based on the given arguments. Well, using TypeScript polymorphism we can specify that, when we call the select or dispatch methods of the @wordpress/data package with the name of our store as a parameter, the result we get is our selectors and our actions respectively.

To do this, simply add a declare module block in the file where we register our store as follows:

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

and then define what the Selectors and Actions types actually are:

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 far, so good, right? The only “problem” is that we have to manually define the Selectors and Actions types, which sounds weird given that TypeScript already knows we have a set of properly-typed selectors and actions

Nelio A/B Testing

Native Tests for WordPress

Use your WordPress page editor to create variants and run powerful tests with just a few clicks. No coding skills required.

Manipulating function types in TypeScript

If we take a look at the types of the actions and selectors objects we imported, we will see that TypeScript tells us the following:

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

As you can see, their types are an exact copy of the types we manually defined in the previous section. Well, almost exact: selectors are missing their first argument (the store state, because it is not present when we call a selector from select) and actions return void (as actions called via dispatch return nothing).

Can we use them to automatically generate the Selectors and Actions types we need?

How to remove the first parameter of a function type in TypeScript

Let’s focus for a moment on the getPost selector. Its type is as follows:

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

As we just said, we need a new function type that doesn’t have the state parameter:

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

So, we need TypeScript to generate a new type from an already-existing type. This can be achieved by combining several advanced functionalities of the language:

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

Complicated, huh? Let’s take a closer look at what’s going on here:

  • type OmitFirstArg<F>. First of all, we define a new auxiliary generic type (OmitFirstArg) . In general, a generic type is a type that let’s us define new types from already-existing types. For instance, you’re probably familiar with the Array<T> type, as it lets you create lists of things: Array<string> is a list of strings, Array<Post> is a list of Post, etc. Well, following this notion, OmitFirstArg<F> is a helper type that removes the first argument of a function.
  • Since this is a generic type, we could theoretically use it with any other TypeScript type. That is, things like OmitFirstArg<string> and OmitFirstArg<Post> are possible… even though we know this type should only be used with functions that have at least one argument. In order to make sure this helper type is used with functions only we’ll define it as a conditional type. The conditional type let’s us specify what the resulting type should be based on a condition: “if F is a function with at least one argument (condition), the resulting type is another function where the first argument has been removed (type when condition is true); otherwise, use the never type (type when condition is false).”
  • F extends XXX. This is the formula to specify the condition. Do you want to check that F is a string? Just type: F extends string. Easy peasy. But what about “a function with one argument?” That sure sounds more complicated…
  • (x: any, ...args: infer P) => infer R. This is a function type: we start with the arguments (in parenthesis), followed by an arrow, followed by the function’s return type. In this particular case, we require that the function have one argument x (whose specific type is irrelevant). This type definition has two interesting bits. On the one hand, we use the rest operator to capture the types P of the remaining args (if any). On the other hand, we use TypeScript’s type inference (infer) to know what those types P really are, as well as the exact return type R.
  • ? (...args: P) => R : never. Finally, we complete the conditional type. If F was a function, the return type is a new function whose arguments are of type P and whose return type is R. If it isn’t, the return type is never.

This is how we can use this helper type to create the new type we wanted:

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

and we’re already one step closer to achieving what we want! Here you can see this example in playground.

How to change the return type of a function type in TypeScript

I’m sure you already know the answer by know: we need an auxiliary generic type that takes a function type and returns a new function type. Something like this:

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

Easy, right? It’s pretty similar to what we did in the previous section: we capture the types of the args in P (there’s no need to require at least one argument x this time) and ignore the return type. If F is a function, return a new function that returns void. Otherwise, return never. Awesome!

Check this out in the playground.

How to map an object type to another object type in TypeScript

Our actions and our selectors are two objects whose keys are the names of those actions and selectors and whose values are the functions themselves. This means that the types of these objects look like the following:

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 the previous two sections we’ve learned how to transform one type of function into another type of function. This means that we could define new types by hand like this:

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

But, of course, this is not sustainable over time: we’re manually specifying the names of the functions in both types. Clearly, we want to automatically map the original type definitions of actions and selectors to new types.

Here’s how you can do that in TypeScript:

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

Hopefully, this already makes sense, but let’s quickly unreel what the previous snippet does anyway:

  • type OmitFirstArgs<O>. We create a new auxiliary generic type that takes an object O.
  • The result is another object type (as the curly braces reveal {...}).
  • [K in keyof O]. We don’t know the exact keys the new object will have, but we do know they have to be the same keys to those included in O. So that’s what we tell TypeScript: we want all keys K that are a keyof O.
  • And then, for each key K, its type is OmitFirstArg<O[K]>. That is, we get the original type (O[K]) and we transform it into the type we want using the auxiliary type we defined (in this case, OmitFirstArg).
  • Finally, we do the same with RemoveReturnTypes and the original auxiliary type RemoveReturnType.

Extending @wordpress/data with our selectors and actions

If you add the four auxiliary types we’ve seen today in a global.d.ts file and save it in the root of your project, you can finally combine everything we have seen in this post to solve the original 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;
}

And that’s it! I hope you liked this dev tip and, if you did, please share it with your colleagues and friends. Oh! And if you know a different approach to get the same result, tell me in the comments.

Featured Image by Gabriel Crismariu on Unsplash.

4 responses to “Adding TypeScript to @wordpress/data Stores”

  1. Daniel Iser Avatar
    Daniel Iser

    Great write up.

    I loved your TS generic mapping functions to remove args & returns. Definitely making use of that.

    That said couple suggestions, some based on changes in @wordpress/data, and a few for TS speficially.

    1. @wordpress/data no longer recommend the registerStore method:

    registerStore( STORE, {...} );

    Instead they prefer creating a store config:

    import { createReduxStore, register } from '@wordpress/data';
    const store = createReduxStore( STORE, {...} );
    register( store );
    

    2. Core is also no longer recommending accessing custom stores via string or string constants like 'nelio-store' or STORE, now they prefer you pass in store const returned above from createReduxStore. Not sure why, but likely for more versatility in accessing entire store.

    3. You can create a new StoreKey type that will work for string, string const & passing store directly like so

    type StoreKey = 'store/name' | typeof STORE_NAME| typeof store;

    4. You can add your TS defs directly to actions & selectors, then infer types via import() directly into a type.

    typeof import( './actions' );

    5. All of this can be done without being added to your runtime code via static declaration files (.d.ts). Assuming you already declared your generic mapping functions somewhere, and also that you have the following file structure:

    /store/
      /selectors.ts
      /actions.ts
      /index.ts (exports STORE_NAME, store)
      /types.d.ts
    

    Then ad this to your types.d.ts file.

    type Selectors = OmitFirstArgs;
    type Actions = RemoveReturnTypes;
    type StoreKey =
    	| 'store/name'
    	| typeof import('./index').STORE_NAME
    	| typeof import('./index').store;
    declare module '@wordpress/data' {
    	function select( key: StoreKey ): Selectors;
    	function dispatch( key: StoreKey ): Actions;
    	function useDispatch( key: StoreKey ): Actions;
    }
    

    6. You’ll notice I added a useDispatch hack also. This is because useDispatches typing currently from core and the @types/wordpress__data are borked, this simple override fixes the expected output when used with your store.

    1. David Aguilera Avatar

      Thank you so much for this comment, Daniel. I really appreciate all your inputs and I’ll be using them from now on.

  2. Payton Swick Avatar
    Payton Swick

    This was really helpful! Thank you!

    > actions return void (as actions called via dispatch return nothing).

    Actually, dispatched actions return Promise, which can sometimes be helpful if you need to know when an action is complete. This is a little unclear from the docs, but you can see it mentioned here:

    https://github.com/WordPress/gutenberg/blob/6aef6e6b9b5cf2f5aeff6145cd24181edf0d43f0/packages/data/README.md#user-content-dispatch

    “Note: Action creators returned by the dispatch will return a promise when they are called.”

    I fixed this in the DT types in https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60693 although as mentioned above the DT types for this package are sometimes not great.

    1. David Aguilera Avatar

      Thanks, Payton! I’ll fix this.

Leave a Reply

Your email address will not be published. Required fields are marked *

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.