diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index c2a53eb5caff47..24186f4ab5dbae 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -269,7 +269,7 @@ Use the [TypeScript `import` function](https://www.typescriptlang.org/docs/handb Since an imported type declaration can occupy an excess of the available line length and become verbose when referenced multiple times, you are encouraged to create an alias of the external type using a `@typedef` declaration at the top of the file, immediately following [the `import` groupings](/docs/contributors/code/coding-guidelines.md#imports). ```js -/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ +/** @typedef {import('@wordpress/rich-text').RichTextValue} RichTextValue */ ``` Note that all custom types defined in another file can be imported. @@ -277,9 +277,9 @@ Note that all custom types defined in another file can be imported. When considering which types should be made available from a WordPress package, the `@typedef` statements in the package's entry point script should be treated as effectively the same as its public API. It is important to be aware of this, both to avoid unintentionally exposing internal types on the public interface, and as a way to expose the public types of a project. ```js -// packages/data/src/index.js +// packages/rich-text/src/can-indent-list-items.js -/** @typedef {import('./registry').WPDataRegistry} WPDataRegistry */ +/** @typedef {import('./create').RichTextValue} RichTextValue */ ``` In this snippet, the `@typedef` will support the usage of the previous example's `import('@wordpress/data')`. diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 8851a2b9b47a15..17f02f7603bed9 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -12,8 +12,6 @@ import useBlockSync from './use-block-sync'; import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; -/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ - function BlockEditorProvider( props ) { const { children, settings } = props; diff --git a/packages/block-editor/src/components/provider/index.native.js b/packages/block-editor/src/components/provider/index.native.js index 8851a2b9b47a15..17f02f7603bed9 100644 --- a/packages/block-editor/src/components/provider/index.native.js +++ b/packages/block-editor/src/components/provider/index.native.js @@ -12,8 +12,6 @@ import useBlockSync from './use-block-sync'; import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; -/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ - function BlockEditorProvider( props ) { const { children, settings } = props; diff --git a/packages/data/README.md b/packages/data/README.md index 2e3e07a13ecc1b..deffa64866aff8 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -387,7 +387,7 @@ _Parameters_ _Returns_ -- `WPDataRegistry`: Data registry. +- `DataRegistry`: Data registry. ### createRegistryControl diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index 0bf50f768269d5..e30b7c981938cd 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -9,7 +9,7 @@ import { merge, isPlainObject } from 'lodash'; import defaultStorage from './storage/default'; import { combineReducers } from '../../'; -/** @typedef {import('../../registry').WPDataRegistry} WPDataRegistry */ +/** @typedef {import('../../types').DataRegistry} DataRegistry */ /** @typedef {import('../../registry').WPDataPlugin} WPDataPlugin */ @@ -116,7 +116,7 @@ export function createPersistenceInterface( options ) { /** * Data plugin to persist store state into a single storage key. * - * @param {WPDataRegistry} registry Data registry. + * @param {DataRegistry} registry Data registry. * @param {?WPDataPersistencePluginOptions} pluginOptions Plugin options. * * @return {WPDataPlugin} Data plugin. diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index f31cc1953828b6..efe530ee33f887 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -16,26 +16,7 @@ import coreDataStore from './store'; import { createEmitter } from './utils/emitter'; /** @typedef {import('./types').StoreDescriptor} StoreDescriptor */ - -/** - * @typedef {Object} WPDataRegistry An isolated orchestrator of store registrations. - * - * @property {Function} registerGenericStore Given a namespace key and settings - * object, registers a new generic - * store. - * @property {Function} registerStore Given a namespace key and settings - * object, registers a new namespace - * store. - * @property {Function} subscribe Given a function callback, invokes - * the callback on any change to state - * within any registered store. - * @property {Function} select Given a namespace key, returns an - * object of the store's registered - * selectors. - * @property {Function} dispatch Given a namespace key, returns an - * object of the store's registered - * action dispatchers. - */ +/** @typedef {import('./types').DataRegistry} DataRegistry */ /** * @typedef {Object} WPDataPlugin An object of registry function overrides. @@ -50,7 +31,7 @@ import { createEmitter } from './utils/emitter'; * @param {Object} storeConfigs Initial store configurations. * @param {Object?} parent Parent registry. * - * @return {WPDataRegistry} Data registry. + * @return {DataRegistry} Data registry. */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; diff --git a/packages/data/src/resolvers-cache-middleware.js b/packages/data/src/resolvers-cache-middleware.js index 5b71239b0f0089..521c76e65666da 100644 --- a/packages/data/src/resolvers-cache-middleware.js +++ b/packages/data/src/resolvers-cache-middleware.js @@ -8,12 +8,12 @@ import { get } from 'lodash'; */ import coreDataStore from './store'; -/** @typedef {import('./registry').WPDataRegistry} WPDataRegistry */ +/** @typedef {import('./types').DataRegistry} DataRegistry */ /** * Creates a middleware handling resolvers cache invalidation. * - * @param {WPDataRegistry} registry The registry reference for which to create + * @param {DataRegistry} registry The registry reference for which to create * the middleware. * @param {string} reducerKey The namespace for which to create the * middleware. diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts index 6094aa19674b55..c8392f42a1707d 100644 --- a/packages/data/src/types.ts +++ b/packages/data/src/types.ts @@ -1,8 +1,14 @@ +/** + * External dependencies + */ +import type { MutableRefObject } from 'react'; +import type { Store as ReduxStore } from 'redux'; + type MapOf< T > = { [ name: string ]: T }; -export type ActionCreator = Function | Generator; -export type Resolver = Function | Generator; -export type Selector = Function; +export type ActionCreator = ( ...args: any[] ) => any | Generator; +export type Resolver = ( ...args: any[] ) => any | Generator; +export type Selector = ( ...args: any[] ) => any; export type AnyConfig = ReduxStoreConfig< any, any, any >; @@ -32,18 +38,271 @@ export interface ReduxStoreConfig< initialState?: State; reducer: ( state: any, action: any ) => any; actions?: ActionCreators; - resolvers?: MapOf< Resolver >; + resolvers?: ResolversOf< Selectors >; selectors?: Selectors; controls?: MapOf< Function >; } +/** Unsubscribes from a registered listener. */ +export interface Unsubscriber { + (): void; +} + +export interface DataStores {} + +export interface Stores extends DataStores {} + +/** + * Returns a store config given a store name or an actual store config. + * + * Functions in the data registry typically accept either the name of + * a store, e.g. 'core/editor', or they accept the actual store + * configuration object, e.g. 'import { storeConfig } from '@wordpress/editor'`. + * + * This type is a convenience wrapper for turning that reference into + * an actual store regardless of what was passed. + * + * Warning! Will fail if given a name not already registered. In such a + * case add an ambient declaration for `@wordpress/data` with the store + * name and configuration so that it can merge into the catalog of + * registered stores. + */ +type Store< + StoreRef extends Stores[ keyof Stores ] | keyof Stores +> = StoreRef extends AnyConfig + ? StoreRef + : StoreRef extends keyof Stores + ? Stores[ StoreRef ] + : never; + +/** + * Removes the first argument from a function. + * + * By default, it removes 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. + * + * It is possible to manually provide a custom curried signature + * and avoid the automatic inference. When the + * F generic argument passed to this helper extends the + * SelectorWithCustomCurrySignature type, the F['CurriedSignature'] + * property is used verbatim. + * + * This is useful because TypeScript does not correctly remove + * arguments from complex function signatures constrained by + * interdependent generic parameters. + * For more context, see https://github.com/WordPress/gutenberg/pull/41578 + */ +type CurriedState< F > = F extends SelectorWithCustomCurrySignature + ? F[ 'CurriedSignature' ] + : F extends ( state: any, ...args: infer P ) => infer R + ? ( ...args: P ) => R + : F; +/** + * Utility to manually specify curried selector signatures. + * + * It comes handy when TypeScript can't automatically produce the + * correct curried function signature. For example: + * + * ```ts + * type BadlyInferredSignature = CurriedState< + * ( + * state: any, + * kind: K, + * key: K extends string ? 'one value' : false + * ) => K + * > + * // BadlyInferredSignature evaluates to: + * // (kind: string number, key: false "one value") => string number + * ``` + * + * With SelectorWithCustomCurrySignature, we can provide a custom + * signature and avoid relying on TypeScript inference: + * ```ts + * interface MySelectorSignature extends SelectorWithCustomCurrySignature { + * ( + * state: any, + * kind: K, + * key: K extends string ? 'one value' : false + * ): K; + * + * CurriedSignature: ( + * kind: K, + * key: K extends string ? 'one value' : false + * ): K; + * } + * type CorrectlyInferredSignature = CurriedState + * // (kind: K, key: K extends string ? 'one value' : false): K; + * + * For even more context, see https://github.com/WordPress/gutenberg/pull/41578 + * ``` + */ +export interface SelectorWithCustomCurrySignature { + __isCurryContainer: true; + CurriedSignature: Function; +} + +/** + * Returns a function whose return type is a Promise of the given return type. + * + * This is designed to take existing selectors and return a resolveSelector + * version of it which returns a Promise of the original return type. Promises + * flatten naturally and so if the original function already returned a Promise + * we can reuse the existing function type. + */ +type Resolvable< + F extends ( ...args: any[] ) => any +> = ReturnType< F > extends Promise< any > + ? F + : ( ...args: Parameters< F > ) => Promise< ReturnType< F > >; + +/** + * "Unwraps" thunks, whereby Redux considers any function is a thunk. + */ +type ResolvedThunks< Actions extends AnyConfig[ 'actions' ] > = { + [ Action in keyof Actions ]: Actions[ Action ] extends ( + ...args: infer P + ) => ( ...thunkArgs: any ) => infer R + ? ( ...args: P ) => R + : Actions[ Action ]; +}; + +/** + * For every defined selector returns a function with the same arguments + * except for the initial `state` parameter. + * + * Excludes specific fields that are ignored when creating resolvers. + * + * @see mapResolveSelectors + */ +type ResolversOf< Selectors extends MapOf< Selector > > = NonNullable< + { + [ Name in keyof Selectors as Exclude< + Name, + NonResolveSelectFields + > ]?: Resolvable< CurriedState< Selectors[ Name ] > >; + } +>; + +/** + * These fields are excluded from the resolveSelect mapping. + * + * @see mapResolveSelectors + */ +type NonResolveSelectFields = + | 'getIsResolving' + | 'hasStartedResolution' + | 'hasFinishedResolution' + | 'isResolving' + | 'getCachedResolvers'; + export interface DataRegistry { - register: ( store: StoreDescriptor< any > ) => void; + /** Apply multiple store updates without calling store listeners until all have finished */ + batch( executor: () => void ): void; + + /** Returns the available actions for a given store. */ + dispatch< StoreRef extends Stores[ keyof Stores ] | keyof Stores >( + store: StoreRef + ): ResolvedThunks< NonNullable< Store< StoreRef >[ 'actions' ] > >; + + /** Registers a new store into the registry. */ + register( store: StoreDescriptor< any > ): void; + + /** Given a namespace key and store description, registers a new Redux store into the registry. */ + registerStore( name: string, store: StoreDescriptor< any > ): ReduxStore; + + /** + * Returns a version of the available selectors for a given store that returns + * a Promise which resolves after all associated resolvers have finished. + */ + resolveSelect< + StoreRef extends Stores[ keyof Stores ] | keyof Stores, + Selectors extends Store< StoreRef >[ 'selectors' ] + >( + store: StoreRef + ): Selectors extends MapOf< Selector > + ? Required< ResolversOf< Selectors > > + : {}; + + /** Returns the available selectors for a given store. */ + select< + StoreRef extends Stores[ keyof Stores ] | keyof Stores, + Selectors extends Store< StoreRef >[ 'selectors' ] + >( + store: StoreRef + ): NonNullable< + { + [ Name in keyof Selectors ]: CurriedState< Selectors[ Name ] >; + } + >; + + /** Raw access to the underlying store instances in the registry. */ + stores: Readonly< + { + [ Name in keyof Stores ]: StoreInstance< Stores[ Name ] >; + } + >; + + /** Subscribe to changes from all registered stores. */ + subscribe( listener: () => void ): Unsubscriber; + + // Unstable and/or experimental methods + + /** Used internally by useSelect to track which stores are queried by `select` calls. */ + __experimentalMarkListeningStores< + Callback extends ( ...args: any[] ) => any + >( + callback: Callback, + listeners: MutableRefObject< string[] > + ): ReturnType< Callback >; + + /** Subscribe to changes in stores that are currently queried by useSelect. */ + __experimentalSubscribeStore( + name: string, + listener: () => void + ): Unsubscriber; + + // Deprecated methods and properties + + /** + * Legacy reference to set of store isntances in the registry. + * + * @deprecated Use registry.stores instead. + */ + namespaces: Readonly< + { + [ Name in keyof Stores ]: StoreInstance< Stores[ Name ] >; + } + >; + + /** + * Given a namespace key and settings object, registers a new store into the registry. + * + * @deprecated Use register( store ) instead. + */ + registerGenericStore( name: string, store: StoreDescriptor< any > ): void; + + /** + * Returns an enhanced version of the given registry. + * + * @deprecated + */ + use< + Source extends DataRegistry, + Enhancement extends Partial< Source > & Record< string, any >, + Options + >( + plugin: ( registry: Source, options?: Options ) => Enhancement, + options?: Options + ): Source & Enhancement; } export interface DataEmitter { emit: () => void; - subscribe: ( listener: () => void ) => () => void; + subscribe: ( listener: () => void ) => Unsubscriber; pause: () => void; resume: () => void; isPaused: boolean; @@ -54,7 +313,7 @@ export interface DataEmitter { type ActionCreatorsOf< Config extends AnyConfig > = Config extends ReduxStoreConfig< any, infer ActionCreators, any > - ? { [ name in keyof ActionCreators ]: Function | Generator } + ? ActionCreators : never; type SelectorsOf< Config extends AnyConfig > = Config extends ReduxStoreConfig< @@ -62,5 +321,5 @@ type SelectorsOf< Config extends AnyConfig > = Config extends ReduxStoreConfig< any, infer Selectors > - ? { [ name in keyof Selectors ]: Function } + ? Selectors : never;