Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promisify action creator return type for WP data dispatch #52530

Merged
merged 11 commits into from
Jul 20, 2023
13 changes: 9 additions & 4 deletions packages/data/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Bug Fix

- Update the type definitions for dispatched actions by accounting for Promisified return values and thunks. Previously, a dispatched action's return type was the same as the return type of the original action creator, which did not account for how dispatch works internally. (Plain actions get wrapped in a Promise, and thunk actions ultimately resolve to the innermost function's return type).
- Update the type definition for dispatch() to handle string store descriptors correctly.

## 9.8.0 (2023-07-20)

## 9.7.0 (2023-07-05)
Expand Down Expand Up @@ -62,7 +67,7 @@

### Breaking Changes

Add TypeScript types to the built package (via "types": "build-types" in the package.json)
– Add TypeScript types to the built package (via "types": "build-types" in the package.json)

### Bug Fix

Expand Down Expand Up @@ -100,9 +105,9 @@

### New Features

- Enabled thunks by default for all stores and removed the `__experimentalUseThunks` flag.
- Store the resolution errors in store metadata and expose them using `hasResolutionFailed` the `getResolutionError` meta-selectors ([#38669](https://github.com/WordPress/gutenberg/pull/38669)).
- Expose the resolution status (undefined, resolving, finished, error) via the `getResolutionState` meta-selector ([#38669](https://github.com/WordPress/gutenberg/pull/38669)).
- Enabled thunks by default for all stores and removed the `__experimentalUseThunks` flag.
- Store the resolution errors in store metadata and expose them using `hasResolutionFailed` the `getResolutionError` meta-selectors ([#38669](https://github.com/WordPress/gutenberg/pull/38669)).
- Expose the resolution status (undefined, resolving, finished, error) via the `getResolutionState` meta-selector ([#38669](https://github.com/WordPress/gutenberg/pull/38669)).

## 6.2.1 (2022-02-10)

Expand Down
4 changes: 2 additions & 2 deletions packages/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,11 @@ dispatch( myCustomStore ).setPrice( 'hammer', 9.75 );

_Parameters_

- _storeNameOrDescriptor_ `string | T`: The store descriptor. The legacy calling convention of passing the store name is also supported.
- _storeNameOrDescriptor_ `StoreNameOrDescriptor`: The store descriptor. The legacy calling convention of passing the store name is also supported.

_Returns_

- `ActionCreatorsOf< ConfigOf< T > >`: Object containing the action creators.
- `DispatchReturn< StoreNameOrDescriptor >`: Object containing the action creators.

### plugins

Expand Down
15 changes: 6 additions & 9 deletions packages/data/src/dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
/**
* Internal dependencies
*/
import type {
ActionCreatorsOf,
AnyConfig,
ConfigOf,
StoreDescriptor,
} from './types';
import type { AnyConfig, StoreDescriptor, DispatchReturn } from './types';
import defaultRegistry from './default-registry';

/**
Expand All @@ -28,8 +23,10 @@ import defaultRegistry from './default-registry';
* ```
* @return Object containing the action creators.
*/
export function dispatch< T extends StoreDescriptor< AnyConfig > >(
storeNameOrDescriptor: string | T
): ActionCreatorsOf< ConfigOf< T > > {
Copy link
Member Author

@noahtallen noahtallen Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While testing in another repo, I came across an error where dispatch( 'store-name' ) had the return type ActionCreatorsOf.... This return type doesn't work, because we aren't using the object format of the store name. useDispatch handles this correctly (the return type should be unknown or similar, since we don't know any metadata about the store), so this basically makes it more similar to those types.

export function dispatch<
StoreNameOrDescriptor extends StoreDescriptor< AnyConfig > | string
>(
storeNameOrDescriptor: StoreNameOrDescriptor
): DispatchReturn< StoreNameOrDescriptor > {
return defaultRegistry.dispatch( storeNameOrDescriptor );
}
43 changes: 38 additions & 5 deletions packages/data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { combineReducers as reduxCombineReducers } from 'redux';

type MapOf< T > = { [ name: string ]: T };

export type ActionCreator = Function | Generator;
export type ActionCreator = ( ...args: any[] ) => any | Generator;
Copy link
Member Author

@noahtallen noahtallen Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function doesn't let you add return types and isn't compatible with the arrow syntax, so it needs to be changed so that we can wrap around the type below.

export type Resolver = Function | Generator;
export type Selector = Function;

Expand Down Expand Up @@ -43,13 +43,15 @@ export interface ReduxStoreConfig<
controls?: MapOf< Function >;
}

// Return type for the useSelect() hook.
export type UseSelectReturn< F extends MapSelect | StoreDescriptor< any > > =
F extends MapSelect
? ReturnType< F >
: F extends StoreDescriptor< any >
? CurriedSelectorsOf< F >
: never;

// Return type for the useDispatch() hook.
export type UseDispatchReturn< StoreNameOrDescriptor > =
StoreNameOrDescriptor extends StoreDescriptor< any >
? ActionCreatorsOf< ConfigOf< StoreNameOrDescriptor > >
Expand All @@ -59,9 +61,12 @@ export type UseDispatchReturn< StoreNameOrDescriptor > =

export type DispatchFunction = < StoreNameOrDescriptor >(
store: StoreNameOrDescriptor
) => StoreNameOrDescriptor extends StoreDescriptor< any >
? ActionCreatorsOf< ConfigOf< StoreNameOrDescriptor > >
: any;
) => DispatchReturn< StoreNameOrDescriptor >;

export type DispatchReturn< StoreNameOrDescriptor > =
StoreNameOrDescriptor extends StoreDescriptor< any >
? ActionCreatorsOf< ConfigOf< StoreNameOrDescriptor > >
: unknown;

export type MapSelect = (
select: SelectFunction,
Expand Down Expand Up @@ -170,9 +175,37 @@ export type ConfigOf< S > = S extends StoreDescriptor< infer C > ? C : never;

export type ActionCreatorsOf< Config extends AnyConfig > =
Config extends ReduxStoreConfig< any, infer ActionCreators, any >
? ActionCreators
? PromisifiedActionCreators< ActionCreators >
: never;

// Takes an object containing all action creators for a store and updates the
// return type of each action creator to account for internal registry details --
// for example, dispatched actions are wrapped with a Promise.
export type PromisifiedActionCreators<
ActionCreators extends MapOf< ActionCreator >
> = {
[ Action in keyof ActionCreators ]: PromisifyActionCreator<
ActionCreators[ Action ]
>;
};

// Wraps action creator return types with a Promise and handles thunks.
export type PromisifyActionCreator< Action extends ActionCreator > = (
...args: Parameters< Action >
) => Promise<
ReturnType< Action > extends ( ..._args: any[] ) => any
? ThunkReturnType< Action >
: ReturnType< Action >
>;

// A thunk is an action creator which returns a function, which can optionally
// return a Promise. The double ReturnType unwraps the innermost function's
// return type, and Awaited gets the type the Promise resolves to. If the return
// type is not a Promise, Awaited returns that original type.
export type ThunkReturnType< Action extends ActionCreator > = Awaited<
ReturnType< ReturnType< Action > >
>;

type SelectorsOf< Config extends AnyConfig > = Config extends ReduxStoreConfig<
any,
any,
Expand Down