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

Autocompletion support for useSelect (via jsDoc annotations) #41596

Merged
merged 16 commits into from
Jun 23, 2022
Merged
25 changes: 16 additions & 9 deletions packages/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,16 @@ const reduxStore = createStore();

const boundSelectors = mapValues(
existingSelectors,
( selector ) => ( ...args ) => selector( reduxStore.getState(), ...args )
( selector ) =>
( ...args ) =>
selector( reduxStore.getState(), ...args )
);

const boundActions = mapValues( existingActions, ( action ) => ( ...args ) =>
reduxStore.dispatch( action( ...args ) )
const boundActions = mapValues(
existingActions,
( action ) =>
( ...args ) =>
reduxStore.dispatch( action( ...args ) )
);

const genericStore = {
Expand Down Expand Up @@ -369,11 +374,11 @@ const store = createReduxStore( 'demo', {
_Parameters_

- _key_ `string`: Unique namespace identifier.
- _options_ `ReduxStoreConfig`: Registered store options, with properties describing reducer, actions, selectors, and resolvers.
- _options_ `ReduxStoreConfig<State,Actions,Selectors>`: Registered store options, with properties describing reducer, actions, selectors, and resolvers.

_Returns_

- `StoreDescriptor`: Store Object.
- `StoreDescriptor<ReduxStoreConfig<State,Actions,Selectors>>`: Store Object.

### createRegistry

Expand Down Expand Up @@ -431,7 +436,9 @@ that allows to select data from the store's `state`, a registry selector
has signature:

```js
( select ) => ( state, ...selectorArgs ) => result;
( select ) =>
( state, ...selectorArgs ) =>
result;
```

that supports also selecting from other registered stores.
Expand Down Expand Up @@ -834,12 +841,12 @@ function Paste( { children } ) {

_Parameters_

- _mapSelect_ `Function|StoreDescriptor|string`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. When a store key is passed, all selectors for the store will be returned. This is only meant for usage of these selectors in event callbacks, not for data needed to create the element tree.
- _deps_ `Array`: If provided, this memoizes the mapSelect so the same `mapSelect` is invoked on every state change unless the dependencies change.
- _mapSelect_ `T`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. When a store key is passed, all selectors for the store will be returned. This is only meant for usage of these selectors in event callbacks, not for data needed to create the element tree.
- _deps_ `unknown[]`: If provided, this memoizes the mapSelect so the same `mapSelect` is invoked on every state change unless the dependencies change.

_Returns_

- `Function`: A custom react hook.
- `UseSelectReturn<T>`: A custom react hook.
Copy link
Member

Choose a reason for hiding this comment

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

unfortunately until we get hyperlinked or better-resolved types I feel like this is just as useless as Function before it. not a knock against this PR - nothing I think we can do about it now. maybe it's a little better since you can search for UseSelectReturn in the code…


### useSuspenseSelect

Expand Down
41 changes: 24 additions & 17 deletions packages/data/src/components/use-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,36 @@ import useAsyncMode from '../async-mode-provider/use-async-mode';
const noop = () => {};
const renderQueue = createQueue();

/** @typedef {import('../../types').StoreDescriptor} StoreDescriptor */
/**
* @typedef {import('../../types').StoreDescriptor<C>} StoreDescriptor
* @template C
*/
/**
* @typedef {import('../../types').ReduxStoreConfig<State,Actions,Selectors>} ReduxStoreConfig
* @template State,Actions,Selectors
*/
/**
* @typedef {import('../../types').UseSelectReturn<T>} UseSelectReturn
* @template T
*/
/** @typedef {import('../../types').MapSelect} MapSelect */

/**
* Custom react hook for retrieving props from registered selectors.
*
* In general, this custom React hook follows the
* [rules of hooks](https://reactjs.org/docs/hooks-rules.html).
*
* @param {Function|StoreDescriptor|string} mapSelect Function called on every state change. The
* returned value is exposed to the component
* implementing this hook. The function receives
* the `registry.select` method on the first
* argument and the `registry` on the second
* argument.
* When a store key is passed, all selectors for
* the store will be returned. This is only meant
* for usage of these selectors in event
* callbacks, not for data needed to create the
* element tree.
* @param {Array} deps If provided, this memoizes the mapSelect so the
* same `mapSelect` is invoked on every state
* change unless the dependencies change.
* @template {MapSelect | StoreDescriptor<any>} T
* @param {T} mapSelect Function called on every state change. The returned value is
* exposed to the component implementing this hook. The function
* receives the `registry.select` method on the first argument
* and the `registry` on the second argument.
* When a store key is passed, all selectors for the store will be
* returned. This is only meant for usage of these selectors in event
* callbacks, not for data needed to create the element tree.
* @param {unknown[]} deps If provided, this memoizes the mapSelect so the same `mapSelect` is
* invoked on every state change unless the dependencies change.
*
* @example
* ```js
Expand Down Expand Up @@ -86,8 +94,7 @@ const renderQueue = createQueue();
* return <div onPaste={ onPaste }>{ children }</div>;
* }
* ```
*
* @return {Function} A custom react hook.
* @return {UseSelectReturn<T>} A custom react hook.
*/
export default function useSelect( mapSelect, deps ) {
const hasMappingFunction = 'function' === typeof mapSelect;
Expand Down
21 changes: 14 additions & 7 deletions packages/data/src/redux-store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ import * as metadataSelectors from './metadata/selectors';
import * as metadataActions from './metadata/actions';

/** @typedef {import('../types').DataRegistry} DataRegistry */
/** @typedef {import('../types').StoreDescriptor} StoreDescriptor */
/** @typedef {import('../types').ReduxStoreConfig} ReduxStoreConfig */
/**
* @typedef {import('../types').StoreDescriptor<C>} StoreDescriptor
* @template C
*/
/**
* @typedef {import('../types').ReduxStoreConfig<State,Actions,Selectors>} ReduxStoreConfig
* @template State,Actions,Selectors
*/

const trimUndefinedValues = ( array ) => {
const result = [ ...array ];
Expand Down Expand Up @@ -83,12 +89,13 @@ function createResolversCache() {
* } );
* ```
*
* @param {string} key Unique namespace identifier.
* @param {ReduxStoreConfig} options Registered store options, with properties
* describing reducer, actions, selectors,
* and resolvers.
* @template State,Actions,Selectors
* @param {string} key Unique namespace identifier.
* @param {ReduxStoreConfig<State,Actions,Selectors>} options Registered store options, with properties
* describing reducer, actions, selectors,
* and resolvers.
*
* @return {StoreDescriptor} Store Object.
* @return {StoreDescriptor<ReduxStoreConfig<State,Actions,Selectors>>} Store Object.
*/
export default function createReduxStore( key, options ) {
return {
Expand Down
45 changes: 39 additions & 6 deletions packages/data/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface StoreDescriptor< Config extends AnyConfig > {
export interface ReduxStoreConfig<
State,
ActionCreators extends MapOf< ActionCreator >,
Selectors extends MapOf< Selector >
Selectors
> {
initialState?: State;
reducer: ( state: any, action: any ) => any;
Expand All @@ -37,6 +37,40 @@ export interface ReduxStoreConfig<
controls?: MapOf< Function >;
}

export type UseSelectReturn< F extends MapSelect | StoreDescriptor< any > > =
F extends MapSelect
? ReturnType< F >
: F extends StoreDescriptor< any >
? CurriedSelectorsOf< F >
: never;

export type MapSelect = ( select: SelectFunction ) => any;

export type SelectFunction = < S >( store: S ) => CurriedSelectorsOf< S >;

export type CurriedSelectorsOf< S > = S extends StoreDescriptor<
ReduxStoreConfig< any, any, infer Selectors >
>
? { [ key in keyof Selectors ]: CurriedState< Selectors[ key ] > }
: never;

/**
* Removes the first argument from a function
*
* This is designed to remove the `state` parameter from
* registered selectors since that argument is supplied
* by the editor when calling `select(…)`.
*
* For functions with no arguments, which some selectors
* are free to define, returns the original function.
*/
export type CurriedState< F > = F extends (
state: any,
...args: infer P
) => infer R
? ( ...args: P ) => R
: F;
Copy link
Member

Choose a reason for hiding this comment

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

what was the issue with this inference? is it that we lose the generic type parameters through this inference? so if we had const get<T> = (state, id): T => state[id]; that it widens to any?

Copy link
Contributor Author

@adamziel adamziel Jun 15, 2022

Choose a reason for hiding this comment

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

More or less:

type BadlyInferredSignature = CurriedState<
    <K extends string | number>(
        state: any,
        kind: K,
        key: K extends string ? 'one value' : false
    ) => K
>
// BadlyInferredSignature evaluates to:
// (kind: string number, key: false | "one value") => string number

The only solution I found is quite convoluted so I went for shipping the simple Curry type in this PR. We can always ship an updated version in a follow-up PR.


export interface DataRegistry {
register: ( store: StoreDescriptor< any > ) => void;
}
Expand All @@ -51,11 +85,10 @@ export interface DataEmitter {

// Type Helpers.

type ActionCreatorsOf<
Config extends AnyConfig
> = Config extends ReduxStoreConfig< any, infer ActionCreators, any >
? { [ name in keyof ActionCreators ]: Function | Generator }
: never;
type ActionCreatorsOf< Config extends AnyConfig > =
Config extends ReduxStoreConfig< any, infer ActionCreators, any >
? { [ name in keyof ActionCreators ]: Function | Generator }
: never;

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