Commit 31ac4c8a authored by Robert Knight's avatar Robert Knight

Determine type of sidebar store automatically

Avoid the need to manually create the `SidebarStore` type by inferring
it automatically. This works as follows:

 1. The `modules` argument to `createStore` has been converted to a
    tuple type (`[ModuleA, ModuleB, ...]`)
 2. The type of a module that would result from merging all the
    individual modules (`ModuleA & ModuleB ...`) is computed
    using a `TupleToIntersection` utility type
 3. `StoreFromModule` is used to compute the type of the store that the
    merged module would produce
parent 1326d1a9
...@@ -149,6 +149,24 @@ function assignOnce(target, source) { ...@@ -149,6 +149,24 @@ function assignOnce(target, source) {
return Object.assign(target, source); return Object.assign(target, source);
} }
/**
* @template T
* @typedef {{[K in keyof T]: (x: T[K]) => void }} MapContravariant
*/
/**
* Utility that turns a tuple type `[A, B, C]` into an intersection `A & B & C`.
*
* The implementation is magic adapted from
* https://github.com/microsoft/TypeScript/issues/28323. Roughly speaking it
* works by computing a type that could be assigned to any position in the
* tuple, which must be the intersection of all the tuple element types.
*
* @template T
* @template {Record<number, unknown>} [Temp=MapContravariant<T>]
* @typedef {Temp[number] extends (x: infer U) => unknown ? U : never} TupleToIntersection
*/
/** /**
* Create a Redux store from a set of _modules_. * Create a Redux store from a set of _modules_.
* *
...@@ -173,10 +191,11 @@ function assignOnce(target, source) { ...@@ -173,10 +191,11 @@ function assignOnce(target, source) {
* `use-store.js`. This returns a proxy which enables UI components to observe * `use-store.js`. This returns a proxy which enables UI components to observe
* what store state a component depends upon and re-render when it changes. * what store state a component depends upon and re-render when it changes.
* *
* @param {Module<any,any,any,any>[]} modules * @template {readonly Module<any,any,any,any>[]} Modules
* @param {Modules} modules
* @param {any[]} [initArgs] - Arguments to pass to each state module's `initialState` function * @param {any[]} [initArgs] - Arguments to pass to each state module's `initialState` function
* @param {any[]} [middleware] - List of additional Redux middlewares to use * @param {any[]} [middleware] - List of additional Redux middlewares to use
* @return Store<any,any,any> * @return {StoreFromModule<TupleToIntersection<Modules>>}
*/ */
export function createStore(modules, initArgs = [], middleware = []) { export function createStore(modules, initArgs = [], middleware = []) {
/** @type {Record<string, unknown>} */ /** @type {Record<string, unknown>} */
...@@ -241,7 +260,7 @@ export function createStore(modules, initArgs = [], middleware = []) { ...@@ -241,7 +260,7 @@ export function createStore(modules, initArgs = [], middleware = []) {
} }
Object.assign(store, selectorMethods); Object.assign(store, selectorMethods);
return store; return /** @type {any} */ (store);
} }
/** /**
......
...@@ -17,30 +17,7 @@ import { sidebarPanelsModule } from './modules/sidebar-panels'; ...@@ -17,30 +17,7 @@ import { sidebarPanelsModule } from './modules/sidebar-panels';
import { toastMessagesModule } from './modules/toast-messages'; import { toastMessagesModule } from './modules/toast-messages';
import { viewerModule } from './modules/viewer'; import { viewerModule } from './modules/viewer';
/** /** @typedef {ReturnType<createSidebarStore>} SidebarStore */
* @template M
* @typedef {import('./create-store').StoreFromModule<M>} StoreFromModule
*/
/**
* @typedef {StoreFromModule<activityModule> &
* StoreFromModule<annotationsModule> &
* StoreFromModule<defaultsModule> &
* StoreFromModule<directLinkedModule> &
* StoreFromModule<draftsModule> &
* StoreFromModule<filtersModule> &
* StoreFromModule<framesModule> &
* StoreFromModule<groupsModule> &
* StoreFromModule<linksModule> &
* StoreFromModule<realTimeUpdatesModule> &
* StoreFromModule<routeModule> &
* StoreFromModule<selectionModule> &
* StoreFromModule<sessionModule> &
* StoreFromModule<sidebarPanelsModule> &
* StoreFromModule<toastMessagesModule> &
* StoreFromModule<viewerModule>
* } SidebarStore
*/
/** /**
* Create the central state store for the sidebar application. * Create the central state store for the sidebar application.
...@@ -52,13 +29,14 @@ import { viewerModule } from './modules/viewer'; ...@@ -52,13 +29,14 @@ import { viewerModule } from './modules/viewer';
* [1] https://redux.js.org * [1] https://redux.js.org
* *
* @param {import('../../types/config').SidebarSettings} settings * @param {import('../../types/config').SidebarSettings} settings
* @return {SidebarStore}
* @inject * @inject
*/ */
export function createSidebarStore(settings) { export function createSidebarStore(settings) {
const middleware = [debugMiddleware]; const middleware = [debugMiddleware];
const modules = [ // `const` type gives `modules` a tuple type, which allows `createStore`
// to infer properties (eg. action and selector methods) of returned store.
const modules = /** @type {const} */ ([
activityModule, activityModule,
annotationsModule, annotationsModule,
defaultsModule, defaultsModule,
...@@ -75,8 +53,6 @@ export function createSidebarStore(settings) { ...@@ -75,8 +53,6 @@ export function createSidebarStore(settings) {
sidebarPanelsModule, sidebarPanelsModule,
toastMessagesModule, toastMessagesModule,
viewerModule, viewerModule,
]; ]);
return /** @type {SidebarStore} */ ( return createStore(modules, [settings], middleware);
createStore(modules, [settings], middleware)
);
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment