From 19dd06446508010f071c881a77b8c95e3ee4fa74 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 29 Oct 2023 12:20:53 -0500 Subject: [PATCH 1/5] Fix type issues related to `createStructuredSelector` --- src/createStructuredSelector.ts | 253 +++++++++++++++++++++++++++----- 1 file changed, 214 insertions(+), 39 deletions(-) diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index ecc549610..30eee0c84 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -1,59 +1,234 @@ import { createSelector } from './createSelectorCreator' import type { CreateSelectorFunction } from './createSelectorCreator' -import type { AnyFunction, Head, ObjValueTuple, Selector, Tail } from './types' -import type { MergeParameters } from './versionedTypes' +import type { defaultMemoize } from './defaultMemoize' +import type { + ObjectValuesToTuple, + OutputSelector, + Selector, + UnknownMemoizer +} from './types' +import { assertIsObject } from './utils' interface SelectorsObject { - [key: string]: AnyFunction + [key: string]: Selector } +/** + * It provides a way to create structured selectors. + * The structured selector can take multiple input selectors + * and map their output to an object with specific keys. + * + * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + * + * @public + */ export interface StructuredSelectorCreator { + /** + * A convenience function for a common pattern that arises when using Reselect. + * The selector passed to a `connect` decorator often just takes the values of its input selectors + * and maps them to keys in an object. + * + * @param selectorMap - A key value pair consisting of input selectors. + * @param selectorCreator - A custom selector creator function. It defaults to `createSelector`. + * @returns A memoized structured selector. + * + * @example + * Modern Use Case + * ```ts + * import { createSelector, createStructuredSelector } from 'reselect' + * + * interface State { + * todos: { + * id: number + * title: string + * description: string + * completed: boolean + * }[] + * alerts: { + * id: number + * message: string + * type: 'reminder' | 'notification' + * read: boolean + * }[] + * } + * + * const state: State = { + * todos: [ + * { + * id: 0, + * title: 'Buy groceries', + * description: 'Milk, bread, eggs, and fruits', + * completed: false + * }, + * { + * id: 1, + * title: 'Schedule dentist appointment', + * description: 'Check available slots for next week', + * completed: true + * } + * ], + * alerts: [ + * { + * id: 0, + * message: 'You have an upcoming meeting at 3 PM.', + * type: 'reminder', + * read: false + * }, + * { + * id: 1, + * message: 'New software update available.', + * type: 'notification', + * read: true + * } + * ] + * } + * + * // This: + * const structuredSelector = createStructuredSelector( + * { + * allTodos: (state: State) => state.todos, + * allAlerts: (state: State) => state.alerts, + * selectedTodo: (state: State, id: number) => state.todos[id] + * }, + * createSelector + * ) + * + * // Is essentially the same as this: + * const selector = createSelector( + * [ + * (state: State) => state.todos, + * (state: State) => state.alerts, + * (state: State, id: number) => state.todos[id] + * ], + * (allTodos, allAlerts, selectedTodo) => { + * return { + * allTodos, + * allAlerts, + * selectedTodo + * } + * } + * ) + * ``` + * + * @example + * Simple Use Case + * ```ts + * const selectA = state => state.a + * const selectB = state => state.b + * + * // The result function in the following selector + * // is simply building an object from the input selectors + * const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + * a, + * b + * })) + * + * const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } + * ``` + * + * @template InputSelectorsObject - The shape of the input selectors object. + * @template MemoizeFunction - The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. + * @template ArgsMemoizeFunction - The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. + * + * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + */ < - SelectorMap extends SelectorsObject, - SelectorParams = MergeParameters> + InputSelectorsObject extends SelectorsObject, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + selectorMap: InputSelectorsObject, + selectorCreator?: CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction + > + ): OutputSelector< + ObjectValuesToTuple, + { + [Key in keyof InputSelectorsObject]: ReturnType + }, + MemoizeFunction, + ArgsMemoizeFunction + > + // TODO: Do we need this? + /** + * Second overload + */ + < + State, + Result = State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - selectorMap: SelectorMap, - selectorCreator?: CreateSelectorFunction - ): ( - // Accept an arbitrary number of parameters for all selectors - // The annoying head/tail bit here is because TS isn't convinced that - // the `SelectorParams` type is really an array, so we launder things. - // Plus it matches common usage anyway. - state: Head, - ...params: Tail - ) => { - [Key in keyof SelectorMap]: ReturnType - } - - ( selectors: { - [K in keyof Result]: Selector + [Key in keyof Result]: Selector }, - selectorCreator?: CreateSelectorFunction - ): Selector + selectorCreator?: CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction + > + ): OutputSelector< + readonly Selector[], + Result, + MemoizeFunction, + ArgsMemoizeFunction + > } // Manual definition of state and output arguments -export const createStructuredSelector = (( - selectors: SelectorsObject, - selectorCreator = createSelector +/** + * A convenience function for a common pattern that arises when using Reselect. + * The selector passed to a `connect` decorator often just takes the values of its input selectors + * and maps them to keys in an object. + * + * @example + * Simple Use Case + * ```ts + * const selectA = state => state.a + * const selectB = state => state.b + * + * // The result function in the following selector + * // is simply building an object from the input selectors + * const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + * a, + * b + * })) + * ``` + * + * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + * + * @public + */ +export const createStructuredSelector: StructuredSelectorCreator = (< + InputSelectorsObject extends SelectorsObject, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +>( + inputSelectorsObject: InputSelectorsObject, + selectorCreator: CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction + > = createSelector as CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction + > ) => { - if (typeof selectors !== 'object') { - throw new TypeError( - 'createStructuredSelector expects first argument to be an object ' + - `where each property is a selector, instead received a ${typeof selectors}` - ) - } - const objectKeys = Object.keys(selectors) - const resultSelector = selectorCreator( - objectKeys.map(key => selectors[key]), - (...values: any[]) => { - return values.reduce((composition, value, index) => { - composition[objectKeys[index]] = value + assertIsObject( + inputSelectorsObject, + 'createStructuredSelector expects first argument to be an object ' + + `where each property is a selector, instead received a ${typeof inputSelectorsObject}` + ) + const inputSelectorKeys = Object.keys(inputSelectorsObject) + const dependencies = inputSelectorKeys.map(key => inputSelectorsObject[key]) + const structuredSelector = selectorCreator( + dependencies, + (...inputSelectorResults: any[]) => { + return inputSelectorResults.reduce((composition, value, index) => { + composition[inputSelectorKeys[index]] = value return composition }, {}) } ) - return resultSelector -}) as unknown as StructuredSelectorCreator + return structuredSelector +}) as StructuredSelectorCreator From f787e98d263a391efc37babdeaa6426e78d33257 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 11:29:08 -0500 Subject: [PATCH 2/5] Fix hover preview for `OutputSelector` - Remove left over types related to `ts46-MergeParameters` - Reorganized `types.ts` by category. - Fix `createStructuredSelector` type inference. - Remove second overload of `StructuredSelectorCreator`. - Create `TypedStructuredSelectorCreator` which is used to effectively replace `StructuredSelectorCreator` second function signature (WIP). - Add more JSDocs. - Add `testUtils.ts`. - Add more type tests. - Add more unit tests. - Fix infinite type instantiation issue that was caused inside of `deepNesting` function in `typescript_test\test.ts` file. - Add public and internal tags. - Remove non-existent rules from `.eslintrc`. - Add `@typescript-eslint/consistent-type-imports` rule to help with auto-fixing type imports. --- .eslintrc | 15 +- src/autotrackMemoize/autotrackMemoize.ts | 65 +- src/autotrackMemoize/autotracking.ts | 15 +- src/createSelectorCreator.ts | 76 ++- src/createStructuredSelector.ts | 86 ++- src/defaultMemoize.ts | 47 +- src/index.ts | 34 +- src/types.ts | 728 ++++++++++++++------- src/utils.ts | 66 +- src/versionedTypes/ts47-mergeParameters.ts | 24 + src/weakMapMemoize.ts | 68 +- test/autotrackMemoize.spec.ts | 4 +- test/createStructuredSelector.spec.ts | 75 ++- test/perfComparisons.spec.ts | 4 +- test/reselect.spec.ts | 385 ++++++----- test/testUtils.ts | 196 ++++++ test/tsconfig.json | 1 + typescript_test/argsMemoize.typetest.ts | 226 ++++++- typescript_test/test.ts | 183 +++++- 19 files changed, 1702 insertions(+), 596 deletions(-) create mode 100644 test/testUtils.ts diff --git a/.eslintrc b/.eslintrc index 3f26fda4b..e4f9ff4dc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { "env": { "browser": true, - "node": true, + "node": true }, "extends": "eslint:recommended", "parserOptions": { @@ -14,7 +14,11 @@ "eol-last": 2, "no-multiple-empty-lines": 2, "object-curly-spacing": [2, "always"], - "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], + "quotes": [ + 2, + "single", + { "avoidEscape": true, "allowTemplateLiterals": true } + ], "semi": [2, "never"], "strict": 0, "space-before-blocks": [2, "always"], @@ -45,8 +49,8 @@ "@typescript-eslint/no-use-before-define": ["off"], "@typescript-eslint/ban-types": "off", "prefer-rest-params": "off", - "prefer-rest": "off", - "prefer-spread": "off" + "prefer-spread": "off", + "@typescript-eslint/consistent-type-imports": [2] } }, { @@ -61,9 +65,6 @@ "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/camelcase": "off", - "import/max-dependencies": "off", - "sonarjs/no-duplicate-string": "off", "@typescript-eslint/no-shadow": "off" } } diff --git a/src/autotrackMemoize/autotrackMemoize.ts b/src/autotrackMemoize/autotrackMemoize.ts index 0c8bcd613..e5d698a0e 100644 --- a/src/autotrackMemoize/autotrackMemoize.ts +++ b/src/autotrackMemoize/autotrackMemoize.ts @@ -8,7 +8,68 @@ import { import type { AnyFunction } from '@internal/types' import { createCache } from './autotracking' -export function autotrackMemoize(func: F) { +/** + * Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. + * It uses a Proxy to wrap arguments and track accesses to nested fields + * in your selector on first read. Later, when the selector is called with + * new arguments, it identifies which accessed fields have changed and + * only recalculates the result if one or more of those accessed fields have changed. + * This allows it to be more precise than the shallow equality checks in `defaultMemoize`. + * + * __Design Tradeoffs for `autotrackMemoize`:__ + * - Pros: + * - It is likely to avoid excess calculations and recalculate fewer times than `defaultMemoize` will, + * which may also result in fewer component re-renders. + * - Cons: + * - It only has a cache size of 1. + * - It is slower than `defaultMemoize`, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc) + * - It can have some unexpected behavior. Because it tracks nested field accesses, + * cases where you don't access a field will not recalculate properly. + * For example, a badly-written selector like: + * ```ts + * createSelector([state => state.todos], todos => todos) + * ``` + * that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check. + * + * __Use Cases for `autotrackMemoize`:__ + * - It is likely best used for cases where you need to access specific nested fields + * in data, and avoid recalculating if other fields in the same data objects are immutably updated. + * + * @param func - The function to be memoized. + * @returns A memoized function with a `.clearCache()` method attached. + * + * @example + * Using `createSelector` + * ```ts + * import { unstable_autotrackMemoize as autotrackMemoize, createSelector } from 'reselect' + * + * const selectTodoIds = createSelector( + * [(state: RootState) => state.todos], + * (todos) => todos.map(todo => todo.id), + * { memoize: autotrackMemoize } + * ) + * ``` + * + * @example + * Using `createSelectorCreator` + * ```ts + * import { unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator } from 'reselect' + * + * const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + * + * const selectTodoIds = createSelectorAutotrack( + * [(state: RootState) => state.todos], + * (todos) => todos.map(todo => todo.id) + * ) + * ``` + * + * @template Func - The type of the function that is memoized. + * + * @since 5.0.0 + * @public + * @experimental + */ +export function autotrackMemoize(func: Func) { // we reference arguments instead of spreading them for performance reasons const node: Node> = createNode( @@ -34,5 +95,5 @@ export function autotrackMemoize(func: F) { memoized.clearCache = () => cache.clear() - return memoized as F & { clearCache: () => void } + return memoized as Func & { clearCache: () => void } } diff --git a/src/autotrackMemoize/autotracking.ts b/src/autotrackMemoize/autotracking.ts index c4f2a5abb..b79a50597 100644 --- a/src/autotrackMemoize/autotracking.ts +++ b/src/autotrackMemoize/autotracking.ts @@ -4,7 +4,7 @@ // - https://www.pzuraq.com/blog/how-autotracking-works // - https://v5.chriskrycho.com/journal/autotracking-elegant-dx-via-cutting-edge-cs/ import type { EqualityFn } from '@internal/types' -import { assert } from './utils' +import { assertIsFunction } from '@internal/utils' // The global revision clock. Every time state changes, the clock increments. export let $REVISION = 0 @@ -133,10 +133,11 @@ export function setValue>( storage: T, value: CellValue ): void { - assert( - storage instanceof Cell, - 'setValue must be passed a tracked store created with `createStorage`.' - ) + if (!(storage instanceof Cell)) { + throw new TypeError( + 'setValue must be passed a tracked store created with `createStorage`.' + ) + } storage.value = storage._lastValue = value } @@ -149,8 +150,8 @@ export function createCell( } export function createCache(fn: () => T): TrackingCache { - assert( - typeof fn === 'function', + assertIsFunction( + fn, 'the first parameter to `createCache` must be a function' ) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 6535da7f9..85525ca9c 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -1,3 +1,4 @@ +import type { OutputSelector, Selector, SelectorArray } from 'reselect' import { defaultMemoize } from './defaultMemoize' import type { @@ -7,9 +8,7 @@ import type { ExtractMemoizerFields, GetParamsFromSelectors, GetStateFromSelectors, - OutputSelector, - Selector, - SelectorArray, + InterruptRecursion, StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -28,21 +27,25 @@ import { * * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @public */ export interface CreateSelectorFunction< - MemoizeFunction extends UnknownMemoizer, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > { /** * Creates a memoized selector function. * * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function. - * @returns An output selector. + * @returns A memoized output selector. * * @template InputSelectors - The type of the input selectors as an array. * @template Result - The return type of the `combiner` as well as the output selector. * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} */ ( ...createSelectorArgs: [ @@ -54,18 +57,21 @@ export interface CreateSelectorFunction< Result, MemoizeFunction, ArgsMemoizeFunction - > + > & + InterruptRecursion /** * Creates a memoized selector function. * * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object. - * @returns An output selector. + * @returns A memoized output selector. * * @template InputSelectors - The type of the input selectors as an array. * @template Result - The return type of the `combiner` as well as the output selector. * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} */ < InputSelectors extends SelectorArray, @@ -90,7 +96,8 @@ export interface CreateSelectorFunction< Result, OverrideMemoizeFunction, OverrideArgsMemoizeFunction - > + > & + InterruptRecursion /** * Creates a memoized selector function. @@ -98,12 +105,14 @@ export interface CreateSelectorFunction< * @param inputSelectors - An array of input selectors. * @param combiner - A function that Combines the input selectors and returns an output selector. Otherwise known as the result function. * @param createSelectorOptions - An optional options object that allows for further customization per selector. - * @returns An output selector. + * @returns A memoized output selector. * * @template InputSelectors - The type of the input selectors array. * @template Result - The return type of the `combiner` as well as the output selector. * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} */ < InputSelectors extends SelectorArray, @@ -126,7 +135,8 @@ export interface CreateSelectorFunction< Result, OverrideMemoizeFunction, OverrideArgsMemoizeFunction - > + > & + InterruptRecursion } let globalStabilityCheck: StabilityCheckFrequency = 'once' @@ -149,6 +159,8 @@ let globalStabilityCheck: StabilityCheckFrequency = 'once' * @example * ```ts * import { setInputStabilityCheckEnabled } from 'reselect' +import { assert } from './autotrackMemoize/utils'; +import { OutputSelectorFields, Mapped } from './types'; * * // Run only the first time the selector is called. (default) * setInputStabilityCheckEnabled('once') @@ -161,6 +173,9 @@ let globalStabilityCheck: StabilityCheckFrequency = 'once' * ``` * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} * @see {@link https://github.com/reduxjs/reselect#global-configuration global-configuration} + * + * @since 5.0.0 + * @public */ export function setInputStabilityCheckEnabled( inputStabilityCheckFrequency: StabilityCheckFrequency @@ -195,15 +210,20 @@ export function setInputStabilityCheckEnabled( * * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator} + * + * @since 5.0.0 + * @public */ export function createSelectorCreator< MemoizeFunction extends UnknownMemoizer, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( options: CreateSelectorOptions< - MemoizeFunction, typeof defaultMemoize, - never, + typeof defaultMemoize, + MemoizeFunction, ArgsMemoizeFunction > ): CreateSelectorFunction @@ -230,6 +250,10 @@ export function createSelectorCreator< * ``` * * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * + * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator} + * + * @public */ export function createSelectorCreator( memoize: MemoizeFunction, @@ -280,7 +304,7 @@ export function createSelectorCreator< OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction >( - ...funcs: [ + ...createSelectorArgs: [ ...inputSelectors: [...InputSelectors], combiner: Combiner, createSelectorOptions?: Partial< @@ -309,7 +333,7 @@ export function createSelectorCreator< > = {} // Normally, the result func or "combiner" is the last arg - let resultFunc = funcs.pop() as + let resultFunc = createSelectorArgs.pop() as | Combiner | Partial< CreateSelectorOptions< @@ -324,7 +348,7 @@ export function createSelectorCreator< if (typeof resultFunc === 'object') { directlyPassedOptions = resultFunc // and pop the real result func off - resultFunc = funcs.pop() as Combiner + resultFunc = createSelectorArgs.pop() as Combiner } assertIsFunction( @@ -354,7 +378,7 @@ export function createSelectorCreator< // we wrap it in an array so we can apply it. const finalMemoizeOptions = ensureIsArray(memoizeOptions) const finalArgsMemoizeOptions = ensureIsArray(argsMemoizeOptions) - const dependencies = getDependencies(funcs) as InputSelectors + const dependencies = getDependencies(createSelectorArgs) as InputSelectors const memoizedResultFunc = memoize(function recomputationWrapper() { recomputations++ @@ -370,15 +394,6 @@ export function createSelectorCreator< let firstRun = true // If a selector is called with the exact same arguments we don't need to traverse our dependencies again. - // TODO This was changed to `memoize` in 4.0.0 ( #297 ), but I changed it back. - // The original intent was to allow customizing things like skortchmark's - // selector debugging setup. - // But, there's multiple issues: - // - We don't pass in `memoizeOptions` - // Arguments change all the time, but input values change less often. - // Most of the time shallow equality _is_ what we really want here. - // TODO Rethink this change, or find a way to expose more options? - // @ts-ignore const selector = argsMemoize(function dependenciesChecker() { /** Return values of input selectors which the `resultFunc` takes as arguments. */ const inputSelectorResults = collectInputSelectorResults( @@ -410,7 +425,7 @@ export function createSelectorCreator< lastResult = memoizedResultFunc.apply(null, inputSelectorResults) return lastResult - }, ...finalArgsMemoizeOptions) as Selector< + }, ...finalArgsMemoizeOptions) as unknown as Selector< GetStateFromSelectors, Result, GetParamsFromSelectors @@ -439,5 +454,14 @@ export function createSelectorCreator< > } +/** + * Accepts one or more "input selectors" (either as separate arguments or a single array), + * a single "result function" / "combiner", and an optional options object, and + * generates a memoized selector function. + * + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} + * + * @public + */ export const createSelector = /* #__PURE__ */ createSelectorCreator(defaultMemoize) diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 30eee0c84..11462b925 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -10,6 +10,46 @@ import type { } from './types' import { assertIsObject } from './utils' +/** + * + * @WIP + */ +type SelectorsMap = { + [Key in keyof T]: ReturnType +} + +// TODO: Write more type tests for `TypedStructuredSelectorCreator`. +/** + * Allows you to create a pre-typed version of {@linkcode createStructuredSelector createStructuredSelector} + * For your root state. + * + * @since 5.0.0 + * @public + * @WIP + */ +export interface TypedStructuredSelectorCreator { + < + InputSelectorsObject extends { + [Key in keyof RootState]: Selector + } = { + [Key in keyof RootState]: Selector + }, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + selectors: InputSelectorsObject, + selectorCreator?: CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction + > + ): OutputSelector< + ObjectValuesToTuple, + SelectorsMap, + MemoizeFunction, + ArgsMemoizeFunction + > +} + interface SelectorsObject { [key: string]: Selector } @@ -26,8 +66,8 @@ interface SelectorsObject { export interface StructuredSelectorCreator { /** * A convenience function for a common pattern that arises when using Reselect. - * The selector passed to a `connect` decorator often just takes the values of its input selectors - * and maps them to keys in an object. + * The selector passed to a `connect` decorator often just takes the + * values of its input selectors and maps them to keys in an object. * * @param selectorMap - A key value pair consisting of input selectors. * @param selectorCreator - A custom selector creator function. It defaults to `createSelector`. @@ -145,9 +185,7 @@ export interface StructuredSelectorCreator { > ): OutputSelector< ObjectValuesToTuple, - { - [Key in keyof InputSelectorsObject]: ReturnType - }, + SelectorsMap, MemoizeFunction, ArgsMemoizeFunction > @@ -155,25 +193,25 @@ export interface StructuredSelectorCreator { /** * Second overload */ - < - State, - Result = State, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - >( - selectors: { - [Key in keyof Result]: Selector - }, - selectorCreator?: CreateSelectorFunction< - MemoizeFunction, - ArgsMemoizeFunction - > - ): OutputSelector< - readonly Selector[], - Result, - MemoizeFunction, - ArgsMemoizeFunction - > + // < + // State, + // Result = State, + // MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + // ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + // >( + // selectors: { + // [Key in keyof State]: Selector + // }, + // selectorCreator?: CreateSelectorFunction< + // MemoizeFunction, + // ArgsMemoizeFunction + // > + // ): OutputSelector< + // readonly Selector[], + // Result, + // MemoizeFunction, + // ArgsMemoizeFunction + // > } // Manual definition of state and output arguments diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index c55bd1243..a262e35c3 100644 --- a/src/defaultMemoize.ts +++ b/src/defaultMemoize.ts @@ -87,6 +87,9 @@ function createLruCache(maxSize: number, equals: EqualityFn): Cache { return { get, put, getEntries, clear } } +/** + * @public + */ export const defaultEqualityCheck: EqualityFn = (a, b): boolean => { return a === b } @@ -112,16 +115,50 @@ export function createCacheKeyComparator(equalityCheck: EqualityFn) { } } +/** + * @public + */ export interface DefaultMemoizeOptions { + /** + * Used to compare the individual arguments of the provided calculation function. + * + * @default defaultEqualityCheck + */ equalityCheck?: EqualityFn + /** + * If provided, used to compare a newly generated output value against previous values in the cache. + * If a match is found, the old value is returned. This addresses the common + * ```ts + * todos.map(todo => todo.id) + * ``` + * use case, where an update to another field in the original data causes a recalculation + * due to changed references, but the output is still effectively the same. + */ resultEqualityCheck?: EqualityFn + /** + * The cache size for the selector. If greater than 1, the selector will use an LRU cache internally. + * + * @default 1 + */ maxSize?: number } // defaultMemoize now supports a configurable cache size with LRU behavior, // and optional comparison of the result value with existing values -export function defaultMemoize( - func: F, +/** + * The standard memoize function used by `createSelector`. + * @param func - The function to be memoized. + * @param equalityCheckOrOptions - Either an equality check function or an options object. + * @returns A memoized function with a `.clearCache()` method attached. + * + * @template Func - The type of the function that is memoized. + * + * @see {@link https://github.com/reduxjs/reselect#defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck defaultMemoize} + * + * @public + */ +export function defaultMemoize( + func: Func, equalityCheckOrOptions?: EqualityFn | DefaultMemoizeOptions ) { const providedOptions = @@ -165,7 +202,9 @@ export function defaultMemoize( return value } - memoized.clearCache = () => cache.clear() + memoized.clearCache = () => { + cache.clear() + } - return memoized as F & { clearCache: () => void } + return memoized as Func & { clearCache: () => void } } diff --git a/src/index.ts b/src/index.ts index 441dcc89e..c6db78efe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,19 @@ +export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' +export { + createSelector, + createSelectorCreator, + setInputStabilityCheckEnabled +} from './createSelectorCreator' +export type { CreateSelectorFunction } from './createSelectorCreator' +export { createStructuredSelector } from './createStructuredSelector' +export type { + StructuredSelectorCreator, + TypedStructuredSelectorCreator +} from './createStructuredSelector' +export { defaultEqualityCheck, defaultMemoize } from './defaultMemoize' +export type { DefaultMemoizeOptions } from './defaultMemoize' export type { + Combiner, CreateSelectorOptions, EqualityFn, GetParamsFromSelectors, @@ -9,22 +24,7 @@ export type { ParametricSelector, Selector, SelectorArray, - SelectorResultArray + SelectorResultArray, + StabilityCheckFrequency } from './types' - -export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' - export { weakMapMemoize } from './weakMapMemoize' - -export { defaultEqualityCheck, defaultMemoize } from './defaultMemoize' -export type { DefaultMemoizeOptions } from './defaultMemoize' - -export { - createSelector, - createSelectorCreator, - setInputStabilityCheckEnabled -} from './createSelectorCreator' -export type { CreateSelectorFunction } from './createSelectorCreator' - -export { createStructuredSelector } from './createStructuredSelector' -export type { StructuredSelectorCreator } from './createStructuredSelector' diff --git a/src/types.ts b/src/types.ts index d48f7bb30..c79719a37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,27 +4,28 @@ import type { MergeParameters } from './versionedTypes' export type { MergeParameters } from './versionedTypes' /* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- * * Reselect Data Types * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- */ /** - * A standard selector function, which takes three generic type arguments: - * @template State - The first value, often a Redux root state object - * @template Result - The final result returned by the selector - * @template Params - All additional arguments passed into the selector + * A standard selector function. + * @template State - The first value, often a Redux root state object. + * @template Result - The final result returned by the selector. + * @template Params - All additional arguments passed into the selector. + * + * @public */ export type Selector< - // The state can be anything State = any, - // The result will be inferred Result = unknown, - // There are either 0 params, or N params Params extends readonly any[] = any[] - // If there are 0 params, type the function as just State in, Result out. - // Otherwise, type it as State + Params in, Result out. -> = +> = Distribute< /** * A function that takes a state and returns data that is based on that state. * @@ -33,21 +34,134 @@ export type Selector< * @returns A derived value from the state. */ (state: State, ...params: FallbackIfNever) => Result +> /** - * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. + * An array of input selectors. * - * @template InputSelectors - An array of input selectors. - * @template Result - Result returned by `resultFunc`. + * @public */ -export type Combiner = +export type SelectorArray = readonly Selector[] + +/** + * Extracts an array of all return types from all input selectors. + * + * @public + */ +export type SelectorResultArray = + ExtractReturnType + +/** + * The options object used inside `createSelector` and `createSelectorCreator`. + * + * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object inside `createSelector` to override the original `memoize` function that was initially passed into `createSelectorCreator`. + * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object inside `createSelector` to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. + * + * @public + */ +export interface CreateSelectorOptions< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideMemoizeFunction extends UnknownMemoizer = never, + OverrideArgsMemoizeFunction extends UnknownMemoizer = never +> { /** - * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. + * Overrides the global input stability check for the selector. + * - `once` - Run only the first time the selector is called. + * - `always` - Run every time the selector is called. + * - `never` - Never run the input stability check. * - * @param resultFuncArgs - Return values of input selectors. - * @returns The return value of {@linkcode OutputSelectorFields.resultFunc resultFunc}. + * @default 'once' + * + * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} + * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} + * @see {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} + * + * @since 5.0.0 */ - (...resultFuncArgs: SelectorResultArray) => Result + inputStabilityCheck?: StabilityCheckFrequency + + /** + * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} + * inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * + * When passed directly into `createSelector`, it overrides the `memoize` function initially passed into `createSelectorCreator`. + * + * @example + * ```ts + * import { createSelector, weakMapMemoize } from 'reselect' + * + * const selectTodoById = createSelector( + * [ + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id + * ], + * (todos) => todos[id], + * { memoize: weakMapMemoize } + * ) + * ``` + * + * @since 5.0.0 + */ + // If `memoize` is not provided inside the options object, fallback to `MemoizeFunction` which is the original memoize function passed into `createSelectorCreator`. + memoize: FallbackIfNever + + /** + * The optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * + * When passed directly into `createSelector`, it overrides the `argsMemoize` function initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. + * + * @example + * ```ts + * import { createSelector, weakMapMemoize } from 'reselect' + * + * const selectTodoById = createSelector( + * [ + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id + * ], + * (todos) => todos[id], + * { argsMemoize: weakMapMemoize } + * ) + * ``` + * + * @default defaultMemoize + * + * @since 5.0.0 + */ + // If `argsMemoize` is not provided inside the options object, + // fallback to `ArgsMemoizeFunction` which is the original `argsMemoize` function passed into `createSelectorCreator`. + // If none was passed originally to `createSelectorCreator`, it should fallback to `defaultMemoize`. + argsMemoize?: FallbackIfNever< + OverrideArgsMemoizeFunction, + ArgsMemoizeFunction + > + + /** + * Optional configuration options for the {@linkcode CreateSelectorOptions.memoize memoize} function. + * These options are passed to the {@linkcode CreateSelectorOptions.memoize memoize} function as the second argument. + * + * @since 5.0.0 + */ + // Should dynamically change to the options argument of `memoize`. + memoizeOptions?: OverrideMemoizeOptions< + MemoizeFunction, + OverrideMemoizeFunction + > + + /** + * Optional configuration options for the {@linkcode CreateSelectorOptions.argsMemoize argsMemoize} function. + * These options are passed to the {@linkcode CreateSelectorOptions.argsMemoize argsMemoize} function as the second argument. + * + * @since 5.0.0 + */ + argsMemoizeOptions?: OverrideMemoizeOptions< + ArgsMemoizeFunction, + OverrideArgsMemoizeFunction + > +} /** * The additional fields attached to the output selector generated by `createSelector`. @@ -64,21 +178,18 @@ export type Combiner = * @template Result - The type of the result returned by the `resultFunc`. * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @public */ -export interface OutputSelectorFields< +export type OutputSelectorFields< InputSelectors extends SelectorArray = SelectorArray, Result = unknown, MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> extends Required< - Pick< - CreateSelectorOptions, - 'argsMemoize' | 'memoize' - > - > { - /** The final function passed to `createSelector`. Otherwise known as the `combiner`. */ +> = { + /** The final function passed to `createSelector`. Otherwise known as the `combiner`.*/ resultFunc: Combiner - /** The memoized version of {@linkcode resultFunc resultFunc}. */ + /** The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. */ memoizedResultFunc: Combiner & ExtractMemoizerFields /** Returns the last result calculated by the output selector. */ @@ -89,7 +200,14 @@ export interface OutputSelectorFields< recomputations: () => number /** Resets the count of `recomputations` count to 0. */ resetRecomputations: () => 0 -} +} & Simplify< + Required< + Pick< + CreateSelectorOptions, + 'argsMemoize' | 'memoize' + > + > +> /** * Represents the actual selectors generated by `createSelector`. @@ -98,60 +216,50 @@ export interface OutputSelectorFields< * @template Result - The type of the result returned by the `resultFunc`. * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @public */ export type OutputSelector< InputSelectors extends SelectorArray = SelectorArray, Result = unknown, MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> = PrepareOutputSelector< - InputSelectors, +> = Selector< + GetStateFromSelectors, Result, - MemoizeFunction, - ArgsMemoizeFunction + GetParamsFromSelectors > & - ExtractMemoizerFields + ExtractMemoizerFields & + OutputSelectorFields< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > /** - * A helper type designed to optimize TypeScript performance by composing parts of {@linkcode OutputSelector OutputSelector} in a more statically structured manner. - * - * This is achieved by utilizing the `extends` keyword with `interfaces`, as opposed to creating intersections with type aliases. - * This approach offers some performance benefits: - * - `Interfaces` create a flat object type, while intersections with type aliases recursively merge properties. - * - Type relationships between `interfaces` are also cached, as opposed to intersection types as a whole. - * - When checking against an intersection type, every constituent is verified before checking against the "effective" flattened type. - * - * This optimization focuses on resolving much of the type composition for - * {@linkcode OutputSelector OutputSelector} using `extends` with `interfaces`, - * rather than relying on intersections for the entire {@linkcode OutputSelector OutputSelector}. + * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. * - * @template InputSelectors - The type of the input selectors. - * @template Result - The type of the result returned by the `resultFunc`. - * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * @template InputSelectors - An array of input selectors. + * @template Result - Result returned by `resultFunc`. * - * @see {@link https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections Reference} + * @public */ -export interface PrepareOutputSelector< - InputSelectors extends SelectorArray = SelectorArray, - Result = unknown, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> extends OutputSelectorFields< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - >, - Selector< - GetStateFromSelectors, - Result, - GetParamsFromSelectors - > {} +export type Combiner = Distribute< + /** + * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. + * + * @param resultFuncArgs - Return values of input selectors. + * @returns The return value of {@linkcode OutputSelectorFields.resultFunc resultFunc}. + */ + (...resultFuncArgs: SelectorResultArray) => Result +> /** * A selector that is assumed to have one additional argument, such as - * the props from a React component + * the props from a React component. + * + * @public */ export type ParametricSelector = Selector< State, @@ -159,7 +267,11 @@ export type ParametricSelector = Selector< [Props, ...any] > -/** A generated selector that is assumed to have one additional argument */ +/** + * A generated selector that is assumed to have one additional argument. + * + * @public + */ export type OutputParametricSelector = ParametricSelector< State, Props, @@ -167,127 +279,80 @@ export type OutputParametricSelector = ParametricSelector< > & OutputSelectorFields -/** An array of input selectors */ -export type SelectorArray = ReadonlyArray - -/** A standard function returning true if two values are considered equal */ +/** + * A standard function returning true if two values are considered equal. + * + * @public + */ export type EqualityFn = (a: any, b: any) => boolean -export type StabilityCheckFrequency = 'always' | 'once' | 'never' - /** - * The options object used inside `createSelector` and `createSelectorCreator`. + * The frequency of input stability checks. * - * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. - * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object inside `createSelector` to override the original `memoize` function that was initially passed into `createSelectorCreator`. - * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object inside `createSelector` to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. + * @since 5.0.0 + * @public */ -export interface CreateSelectorOptions< - MemoizeFunction extends UnknownMemoizer, - ArgsMemoizeFunction extends UnknownMemoizer, - OverrideMemoizeFunction extends UnknownMemoizer = never, - OverrideArgsMemoizeFunction extends UnknownMemoizer = never -> { - /** - * Overrides the global input stability check for the selector. - * - `once` - Run only the first time the selector is called. - * - `always` - Run every time the selector is called. - * - `never` - Never run the input stability check. - * - * @default 'once' - * - * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} - * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} - * @see {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} - */ - inputStabilityCheck?: StabilityCheckFrequency - - /** - * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} - * inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * - * When passed directly into `createSelector`, it overrides the `memoize` function initially passed into `createSelectorCreator`. - */ - // If `memoize` is not provided inside the options object, fallback to `MemoizeFunction` which is the original memoize function passed into `createSelectorCreator`. - memoize: FallbackIfNever - - /** - * The optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * - * When passed directly into `createSelector`, it overrides the `argsMemoize` function initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. - * - * @default defaultMemoize - */ - // If `argsMemoize` is not provided inside the options object, - // fallback to `ArgsMemoizeFunction` which is the original `argsMemoize` function passed into `createSelectorCreator`. - // If none was passed originally to `createSelectorCreator`, it should fallback to `defaultMemoize`. - argsMemoize?: FallbackIfNever< - OverrideArgsMemoizeFunction, - ArgsMemoizeFunction - > - - /** - * Optional configuration options for the {@linkcode memoize memoize} function. - * These options are passed to the {@linkcode memoize memoize} function as the second argument. - */ - // Should dynamically change to the options argument of `memoize`. - memoizeOptions?: OverrideMemoizeOptions< - MemoizeFunction, - OverrideMemoizeFunction - > - - /** - * Optional configuration options for the {@linkcode argsMemoize argsMemoize} function. - * These options are passed to the {@linkcode argsMemoize argsMemoize} function as the second argument. - */ - argsMemoizeOptions?: OverrideMemoizeOptions< - ArgsMemoizeFunction, - OverrideArgsMemoizeFunction - > -} +export type StabilityCheckFrequency = 'always' | 'once' | 'never' -/* - * - * Reselect Internal Types +/** + * Determines the combined single "State" type (first arg) from all input selectors. * + * @public */ - -/** Extracts an array of all return types from all input selectors */ -export type SelectorResultArray = - ExtractReturnType - -/** Determines the combined single "State" type (first arg) from all input selectors */ export type GetStateFromSelectors = MergeParameters[0] -/** Determines the combined "Params" type (all remaining args) from all input selectors */ -export type GetParamsFromSelectors< - Selectors extends SelectorArray, - RemainingItems extends readonly unknown[] = Tail> -> = RemainingItems +/** + * Determines the combined "Params" type (all remaining args) from all input selectors. + * + * @public + */ +export type GetParamsFromSelectors = ArrayTail< + MergeParameters +> /* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- * * Reselect Internal Utility Types * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- */ -/** Any function with any arguments */ +/** + * Any function with any arguments. + * + * @internal + */ export type AnyFunction = (...args: any[]) => any -/** Any function with unknown arguments */ + +/** + * Any function with unknown arguments. + * + * @internal + */ export type UnknownFunction = (...args: unknown[]) => unknown -/** Any Memoizer function. A memoizer is a function that accepts another function and returns it. */ -export type UnknownMemoizer = ( - func: Func, - ...options: any[] -) => Func + +/** + * Any Memoizer function. A memoizer is a function that accepts another function and returns it. + * + * @template FunctionType - The type of the function that is memoized. + * + * @internal + */ +export type UnknownMemoizer< + FunctionType extends UnknownFunction = UnknownFunction +> = (func: FunctionType, ...options: any[]) => FunctionType /** * When a generic type parameter is using its default value of `never`, fallback to a different type. * * @template T - Type to be checked. * @template FallbackTo - Type to fallback to if `T` resolves to `never`. + * + * @internal */ export type FallbackIfNever = IfNever @@ -298,6 +363,8 @@ export type FallbackIfNever = IfNever * * @template MemoizeFunction - The type of the `memoize` or `argsMemoize` function initially passed into `createSelectorCreator`. * @template OverrideMemoizeFunction - The type of the optional `memoize` or `argsMemoize` function passed directly into `createSelector` which then overrides the original `memoize` or `argsMemoize` function passed into `createSelectorCreator`. + * + * @internal */ export type OverrideMemoizeOptions< MemoizeFunction extends UnknownMemoizer, @@ -309,53 +376,151 @@ export type OverrideMemoizeOptions< > /** - * Extract the memoize options from the parameters of a memoize function. + * Extracts the non-function part of a type. + * + * @template T - The input type to be refined by excluding function types and index signatures. + * + * @internal + */ +export type NonFunctionType = OmitIndexSignature> + +/** + * Extracts the function part of a type. + * + * @template T - The input type to be refined by extracting function types. + * + * @internal + */ +export type FunctionType = Extract + +/** + * Extracts the options type for a memoization function based on its parameters. + * The first parameter of the function is expected to be the function to be memoized, + * followed by options for the memoization process. * * @template MemoizeFunction - The type of the memoize function to be checked. + * + * @internal */ export type MemoizeOptionsFromParameters< MemoizeFunction extends UnknownMemoizer -> = DropFirstParameter[0] | DropFirstParameter +> = + | ( + | Simplify[0]>> + | FunctionType[0]> + ) + | ( + | Simplify[number]>> + | FunctionType[number]> + )[] /** - * Extracts the additional fields that a memoize function attaches to the function it memoizes (e.g., `clearCache`). + * Extracts the additional fields that a memoize function attaches to + * the function it memoizes (e.g., `clearCache`). * * @template MemoizeFunction - The type of the memoize function to be checked. + * + * @internal */ export type ExtractMemoizerFields = - OmitIndexSignature> + Simplify>> -/** Extract the return type from all functions as a tuple */ -export type ExtractReturnType = { - [index in keyof T]: T[index] extends T[number] ? ReturnType : never +/** + * Extracts the return type from all functions as a tuple. + * + * @internal + */ +export type ExtractReturnType = { + [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] + ? ReturnType + : never } -/** First item in an array */ -export type Head = T extends [any, ...any[]] ? T[0] : never -/** All other items in an array */ -export type Tail = A extends [any, ...infer Rest] ? Rest : never +/** + * Utility type to infer the type of "all params of a function except the first", + * so we can determine what arguments a memoize function accepts. + * + * @internal + */ +export type DropFirstParameter = Func extends ( + firstArg: any, + ...restArgs: infer Rest +) => any + ? Rest + : never -/** Extract only numeric keys from an array type */ -export type AllArrayKeys = A extends any - ? { - [K in keyof A]: K - }[number] +/** + * Distributes over a type. It is used mostly to expand a function type + * in hover previews while preserving their original JSDoc information. + * + * If preserving JSDoc information is not a concern, you can use {@linkcode ExpandFunction ExpandFunction}. + * + * @template T The type to be distributed. + * + * @internal + */ +export type Distribute = T extends T ? T : never + +/** + * Extracts the type of the first element of an array or tuple. + * + * @internal + */ +export type FirstArrayElement = TArray extends readonly [ + unknown, + ...unknown[] +] + ? TArray[0] : never -export type List = ReadonlyArray +/** + * Extracts the type of an array or tuple minus the first element. + * + * @internal + */ +export type ArrayTail = TArray extends readonly [ + unknown, + ...infer TTail +] + ? TTail + : [] + +/** + * An alias for type `{}`. Represents any value that is not `null` or `undefined`. + * It is mostly used for semantic purposes to help distinguish between an + * empty object type and `{}` as they are not the same. + * + * @internal + */ +export type AnyNonNullishValue = NonNullable -export type Has = [U1] extends [U] ? 1 : 0 +/** + * Same as {@linkcode AnyNonNullishValue AnyNonNullishValue} but aliased + * for semantic purposes. It is intended to be used in scenarios where + * a recursive type definition needs to be interrupted to ensure type safety + * and to avoid excessively deep recursion that could lead to performance issues. + * + * @internal + */ +export type InterruptRecursion = AnyNonNullishValue /* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- * * External/Copied Utility Types * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * */ /** * An if-else-like type that resolves depending on whether the given type is `never`. * This is mainly used to conditionally resolve the type of a `memoizeOptions` object based on whether `memoize` is provided or not. * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-never.d.ts Source} + * + * @internal */ export type IfNever = [T] extends [never] ? TypeIfNever @@ -364,7 +529,13 @@ export type IfNever = [T] extends [never] /** * Omit any index signatures from the given object type, leaving only explicitly defined properties. * This is mainly used to remove explicit `any`s from the return type of some memoizers (e.g, `microMemoize`). + * + * __Disclaimer:__ When used on an intersection of a function and an object, + * the function is erased. + * * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/omit-index-signature.d.ts Source} + * + * @internal */ export type OmitIndexSignature = { [KeyType in keyof ObjectType as {} extends Record @@ -374,8 +545,10 @@ export type OmitIndexSignature = { /** * The infamous "convert a union type to an intersection type" hack - * Source: https://github.com/sindresorhus/type-fest/blob/main/source/union-to-intersection.d.ts - * Reference: https://github.com/microsoft/TypeScript/issues/29594 + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/union-to-intersection.d.ts Source} + * @see {@link https://github.com/microsoft/TypeScript/issues/29594 Reference} + * + * @internal */ export type UnionToIntersection = // `extends unknown` is always going to be the case and is used to convert the @@ -394,41 +567,31 @@ export type UnionToIntersection = ? Intersection : never -/** - * Assorted util types for type-level conditional logic - * Source: https://github.com/KiaraGrouwstra/typical - */ -export type Bool = '0' | '1' -export interface Obj { - [k: string]: T -} -export type And = ({ - 1: { 1: '1' } & Obj<'0'> -} & Obj>)[A][B] - -export type Matches = V extends T ? '1' : '0' -export type IsArrayType = Matches - -export type Not = { '1': '0'; '0': '1' }[T] -export type InstanceOf = And, Not>> -export type IsTuple = And< - IsArrayType, - InstanceOf -> - /** * Code to convert a union of values into a tuple. - * Source: https://stackoverflow.com/a/55128956/62937 + * @see {@link https://stackoverflow.com/a/55128956/62937 Source} + * + * @internal */ type Push = [...T, V] +/** + * @see {@link https://stackoverflow.com/a/55128956/62937 Source} + * + * @internal + */ type LastOf = UnionToIntersection< T extends any ? () => T : never > extends () => infer R ? R : never -// TS4.1+ +/** + * TS4.1+ + * @see {@link https://stackoverflow.com/a/55128956/62937 Source} + * + * @internal + */ export type TuplifyUnion< T, L = LastOf, @@ -437,27 +600,66 @@ export type TuplifyUnion< /** * Converts "the values of an object" into a tuple, like a type-level `Object.values()` - * Source: https://stackoverflow.com/a/68695508/62937 + * @see {@link https://stackoverflow.com/a/68695508/62937 Source} + * + * @internal */ -export type ObjValueTuple< +export type ObjectValuesToTuple< T, KS extends any[] = TuplifyUnion, R extends any[] = [] > = KS extends [infer K, ...infer KT] - ? ObjValueTuple + ? ObjectValuesToTuple : R -/** Utility type to infer the type of "all params of a function except the first", so we can determine what arguments a memoize function accepts */ -export type DropFirstParameter = Func extends ( - firstArg: any, - ...restArgs: infer Rest -) => any - ? Rest - : never +/** + * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * + * Type Expansion Utilities + * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * + */ + +/** + * Check whether `U` contains `U1`. + * @see {@link https://millsp.github.io/ts-toolbelt/modules/union_has.html Source} + * + * @internal + */ +export type Has = [U1] extends [U] ? 1 : 0 /** - * Expand an item a single level, or recursively. - * Source: https://stackoverflow.com/a/69288824/62937 + * @internal + */ +export type Boolean2 = 0 | 1 + +/** + * @internal + */ +export type If2 = B extends 1 + ? Then + : Else + +/** + * @internal + */ +export type BuiltIn = + | Function + | Error + | Date + | { readonly [Symbol.toStringTag]: string } + | RegExp + | Generator + +/** + * Expand an item a single level. + * @see {@link https://stackoverflow.com/a/69288824/62937 Source} + * + * @internal */ export type Expand = T extends (...args: infer A) => infer R ? (...args: Expand) => Expand @@ -465,6 +667,12 @@ export type Expand = T extends (...args: infer A) => infer R ? { [K in keyof O]: O[K] } : never +/** + * Expand an item recursively. + * @see {@link https://stackoverflow.com/a/69288824/62937 Source} + * + * @internal + */ export type ExpandRecursively = T extends (...args: infer A) => infer R ? (...args: ExpandRecursively) => ExpandRecursively : T extends object @@ -473,54 +681,68 @@ export type ExpandRecursively = T extends (...args: infer A) => infer R : never : T -type Identity = T +/** + * @internal + */ +export type Identity = T + /** * Another form of type value expansion - * Source: https://github.com/microsoft/TypeScript/issues/35247 + * @see {@link https://github.com/microsoft/TypeScript/issues/35247 Source} + * + * @internal */ export type Mapped = Identity<{ [k in keyof T]: T[k] }> /** - * Fully expand a type, deeply - * Source: https://github.com/millsp/ts-toolbelt (`Any.Compute`) + * This utility type is primarily used to expand a function type in order to + * improve its visual display in hover previews within IDEs. + * + * __Disclaimer:__ Functions expanded using this type will not display their + * original JSDoc information in hover previews. + * + * @template FunctionType - The type of the function to be expanded. + * + * @internal */ +export type ExpandFunction = + FunctionType extends FunctionType + ? (...args: Parameters) => ReturnType + : never -type ComputeDeep = A extends BuiltIn +/** + * Useful to flatten the type output to improve type hints shown in editors. + * And also to transform an interface into a type to aide with assignability. + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts Source} + * + * @internal + */ +export type Simplify = { + [KeyType in keyof T]: T[KeyType] +} & AnyNonNullishValue + +/** + * Fully expand a type, deeply + * @see {@link https://github.com/millsp/ts-toolbelt Any.Compute} + * + * @internal + */ +export type ComputeDeep = A extends BuiltIn ? A : If2< Has, A, - A extends Array - ? A extends Array> - ? Array< - { - [K in keyof A[number]]: ComputeDeep - } & unknown - > + A extends any[] + ? A extends Record[] + ? ({ + [K in keyof A[number]]: ComputeDeep + } & unknown)[] : A - : A extends ReadonlyArray - ? A extends ReadonlyArray> - ? ReadonlyArray< - { - [K in keyof A[number]]: ComputeDeep - } & unknown - > + : A extends readonly any[] + ? A extends readonly Record[] + ? readonly ({ + [K in keyof A[number]]: ComputeDeep + } & unknown)[] : A : { [K in keyof A]: ComputeDeep } & unknown > - -export type If2 = B extends 1 - ? Then - : Else - -export type Boolean2 = 0 | 1 - -export type Key = string | number | symbol - -export type BuiltIn = - | Function - | Error - | Date - | { readonly [Symbol.toStringTag]: string } - | RegExp - | Generator diff --git a/src/utils.ts b/src/utils.ts index eb32abe5c..ceabd2356 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,15 +14,58 @@ import type { * @param errorMessage - An optional custom error message to use if the assertion fails. * @throws A `TypeError` if the assertion fails. */ -export function assertIsFunction( +export function assertIsFunction( func: unknown, errorMessage = `expected a function, instead received ${typeof func}` -): asserts func is Func { +): asserts func is FunctionType { if (typeof func !== 'function') { throw new TypeError(errorMessage) } } +/** + * Assert that the provided value is an object. If the assertion fails, + * a `TypeError` is thrown with an optional custom error message. + * + * @param object - The value to be checked. + * @param errorMessage - An optional custom error message to use if the assertion fails. + * @throws A `TypeError` if the assertion fails. + */ +export function assertIsObject>( + object: unknown, + errorMessage = `expected an object, instead received ${typeof object}` +): asserts object is ObjectType { + if (typeof object !== 'object') { + throw new TypeError(errorMessage) + } +} + +/** + * Assert that the provided array is an array of functions. If the assertion fails, + * a `TypeError` is thrown with an optional custom error message. + * + * @param array - The array to be checked. + * @param errorMessage - An optional custom error message to use if the assertion fails. + * @throws A `TypeError` if the assertion fails. + */ +export function assertIsArrayOfFunctions( + array: unknown[], + errorMessage = `expected all items to be functions, instead received the following types: ` +): asserts array is FunctionType[] { + if ( + !array.every((item): item is FunctionType => typeof item === 'function') + ) { + const itemTypes = array + .map(item => + typeof item === 'function' + ? `function ${item.name || 'unnamed'}()` + : typeof item + ) + .join(', ') + throw new TypeError(`${errorMessage}[${itemTypes}]`) + } +} + /** * Ensure that the input is an array. If it's already an array, it's returned as is. * If it's not an array, it will be wrapped in a new array. @@ -46,21 +89,10 @@ export function getDependencies(createSelectorArgs: unknown[]) { ? createSelectorArgs[0] : createSelectorArgs - if ( - !dependencies.every((dep): dep is Selector => typeof dep === 'function') - ) { - const dependencyTypes = dependencies - .map(dep => - typeof dep === 'function' - ? `function ${dep.name || 'unnamed'}()` - : typeof dep - ) - .join(', ') - - throw new TypeError( - `createSelector expects all input-selectors to be functions, but received the following types: [${dependencyTypes}]` - ) - } + assertIsArrayOfFunctions( + dependencies, + `createSelector expects all input-selectors to be functions, but received the following types: ` + ) return dependencies as SelectorArray } diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 7bd9808cf..5be0dfc43 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -3,6 +3,9 @@ import type { AnyFunction } from '@internal/types' +/** + * @internal + */ type LongestTuple = T extends [ infer U extends unknown[] ] @@ -11,22 +14,37 @@ type LongestTuple = T extends [ ? MostProperties> : never +/** + * @internal + */ type MostProperties = keyof U extends keyof T ? T : U +/** + * @internal + */ type ElementAt = N extends keyof T ? T[N] : unknown +/** + * @internal + */ type ElementsAt = { [K in keyof T]: ElementAt } +/** + * @internal + */ type Intersect = T extends [] ? unknown : T extends [infer H, ...infer T] ? H & Intersect : T[number] +/** + * @internal + */ type MergeTuples< T extends readonly unknown[][], L extends unknown[] = LongestTuple @@ -34,10 +52,16 @@ type MergeTuples< [K in keyof L]: Intersect> } +/** + * @internal + */ type ExtractParameters = { [K in keyof T]: Parameters } +/** + * @internal + */ export type MergeParameters = '0' extends keyof T ? MergeTuples> diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 567128402..5311c9078 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -31,7 +31,71 @@ function createCacheNode(): CacheNode { } } -export function weakMapMemoize(func: F) { +/** + * Creates a tree of `WeakMap`-based cache nodes based on the identity of the + * arguments it's been called with (in this case, the extracted values from your input selectors). + * This allows `weakmapMemoize` to have an effectively infinite cache size. + * Cache results will be kept in memory as long as references to the arguments still exist, + * and then cleared out as the arguments are garbage-collected. + * + * __Design Tradeoffs for `weakmapMemoize`:__ + * - Pros: + * - It has an effectively infinite cache size, but you have no control over + * how long values are kept in cache as it's based on garbage collection and `WeakMap`s. + * - Cons: + * - There's currently no way to alter the argument comparisons. + * They're based on strict reference equality. + * - It's roughly the same speed as `defaultMemoize`, although likely a fraction slower. + * + * __Use Cases for `weakmapMemoize`:__ + * - This memoizer is likely best used for cases where you need to call the + * same selector instance with many different arguments, such as a single + * selector instance that is used in a list item component and called with + * item IDs like: + * ```ts + * useSelector(state => selectSomeData(state, props.category)) + * ``` + * @param func - The function to be memoized. + * @returns A memoized function with a `.clearCache()` method attached. + * + * @example + * Using `createSelector` + * ```ts + * import { createSelector, weakMapMemoize } from 'reselect' + * + * const selectTodoById = createSelector( + * [ + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id + * ], + * (todos) => todos[id], + * { memoize: weakMapMemoize } + * ) + * ``` + * + * @example + * Using `createSelectorCreator` + * ```ts + * import { createSelectorCreator, weakMapMemoize } from 'reselect' + * + * const createSelectorWeakmap = createSelectorCreator(weakMapMemoize) + * + * const selectTodoById = createSelectorWeakmap( + * [ + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id + * ], + * (todos) => todos[id] + * ) + * ``` + * + * @template Func - The type of the function that is memoized. + * + * @since 5.0.0 + * @public + * @experimental + */ +export function weakMapMemoize(func: Func) { // we reference arguments instead of spreading them for performance reasons let fnNode = createCacheNode() @@ -87,5 +151,5 @@ export function weakMapMemoize(func: F) { fnNode = createCacheNode() } - return memoized as F & { clearCache: () => void } + return memoized as Func & { clearCache: () => void } } diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 4180a0c80..12d4a14ce 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,4 +1,4 @@ -import { createSelectorCreator, unstable_autotrackMemoize } from 'reselect' +import { createSelectorCreator, unstable_autotrackMemoize as autotrackMemoize } from 'reselect' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function const numOfStates = 1000000 @@ -24,7 +24,7 @@ for (let i = 0; i < numOfStates; i++) { } describe('Basic selector behavior with autotrack', () => { - const createSelector = createSelectorCreator(unstable_autotrackMemoize) + const createSelector = createSelectorCreator(autotrackMemoize) test('basic selector', () => { // console.log('Selector test') diff --git a/test/createStructuredSelector.spec.ts b/test/createStructuredSelector.spec.ts index 34e177ad3..058c9aba5 100644 --- a/test/createStructuredSelector.spec.ts +++ b/test/createStructuredSelector.spec.ts @@ -1,8 +1,11 @@ import { + createSelector, createSelectorCreator, - defaultMemoize, - createStructuredSelector + createStructuredSelector, + defaultMemoize } from 'reselect' +import type { LocalTestContext, RootState } from './testUtils' +import { setupStore } from './testUtils' interface StateAB { a: number @@ -25,8 +28,8 @@ describe('createStructureSelector', () => { test('structured selector with invalid arguments', () => { expect(() => - // @ts-expect-error createStructuredSelector( + // @ts-expect-error (state: StateAB) => state.a, (state: StateAB) => state.b ) @@ -60,3 +63,69 @@ describe('createStructureSelector', () => { expect(selector({ a: 2, b: 2 })).toEqual({ x: 2, y: 2 }) }) }) + +describe('structured selector created with createStructuredSelector', localTest => { + beforeEach(context => { + const store = setupStore() + context.store = store + context.state = store.getState() + }) + localTest( + 'structured selector created with createStructuredSelector and createSelector are the same', + ({ state }) => { + const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: (state: RootState, id: number) => state.todos[id] + }, + createSelector + ) + const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + (state: RootState, id: number) => state.todos[id] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } + ) + expect(selector(state, 1).selectedTodo.id).toBe( + structuredSelector(state, 1).selectedTodo.id + ) + expect(structuredSelector.dependencies) + .to.be.an('array') + .with.lengthOf(selector.dependencies.length) + expect( + structuredSelector.resultFunc(state.todos, state.alerts, state.todos[0]) + ).toStrictEqual( + selector.resultFunc(state.todos, state.alerts, state.todos[0]) + ) + expect( + structuredSelector.memoizedResultFunc( + state.todos, + state.alerts, + state.todos[0] + ) + ).toStrictEqual( + selector.memoizedResultFunc(state.todos, state.alerts, state.todos[0]) + ) + expect(structuredSelector.argsMemoize).toBe(selector.argsMemoize) + expect(structuredSelector.memoize).toBe(selector.memoize) + expect(structuredSelector.recomputations()).toBe( + selector.recomputations() + ) + expect(structuredSelector.lastResult()).toStrictEqual( + selector.lastResult() + ) + expect(Object.keys(structuredSelector)).toStrictEqual( + Object.keys(selector) + ) + } + ) +}) diff --git a/test/perfComparisons.spec.ts b/test/perfComparisons.spec.ts index a2218d381..2959eca21 100644 --- a/test/perfComparisons.spec.ts +++ b/test/perfComparisons.spec.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { configureStore, createSlice } from '@reduxjs/toolkit' import { - unstable_autotrackMemoize, + unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator, defaultMemoize, weakMapMemoize @@ -19,7 +19,7 @@ describe('More perf comparisons', () => { }) const csDefault = createSelectorCreator(defaultMemoize) - const csAutotrack = createSelectorCreator(unstable_autotrackMemoize) + const csAutotrack = createSelectorCreator(autotrackMemoize) interface Todo { id: number diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 37b03fa38..dfb4abfba 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -1,21 +1,18 @@ // TODO: Add test for React Redux connect function -import type { PayloadAction } from '@reduxjs/toolkit' -import { configureStore, createSlice } from '@reduxjs/toolkit' import lodashMemoize from 'lodash/memoize' import microMemoize from 'micro-memoize' import { - unstable_autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' import type { OutputSelector, OutputSelectorFields } from 'reselect' -// Since Node 16 does not support `structuredClone` -const deepClone = (object: T): T => - JSON.parse(JSON.stringify(object)) +import type { LocalTestContext, RootState } from './testUtils' +import { addTodo, deepClone, setupStore, toggleCompleted } from './testUtils' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function const numOfStates = 1000000 @@ -426,8 +423,7 @@ describe('Customizing selectors', () => { selectorOriginal(deepClone(state)) selectorOriginal(deepClone(state)) const selectorDefaultParametric = createSelector( - (state: State, id: number) => id, - (state: State) => state.todos, + [(state: State, id: number) => id, (state: State) => state.todos], (id, todos) => todos.filter(todo => todo.id === id) ) selectorDefaultParametric(state, 1) @@ -435,65 +431,22 @@ describe('Customizing selectors', () => { }) }) -interface TodoState { - todos: { - id: number - completed: boolean - }[] -} - -const initialState: TodoState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] -} - -const todoSlice = createSlice({ - name: 'todos', - initialState, - reducers: { - toggleCompleted: (state, action: PayloadAction) => { - const todo = state.todos.find(todo => todo.id === action.payload) - if (todo) { - todo.completed = !todo.completed - } - }, - - addTodo: (state, action: PayloadAction) => { - const newTodo = { id: action.payload, completed: false } - state.todos.push(newTodo) - } - } -}) - -const store = configureStore({ - reducer: todoSlice.reducer -}) - -const setupStore = () => - configureStore({ - reducer: todoSlice.reducer - }) - -type LocalTestContext = Record<'store', typeof store> - -describe('argsMemoize and memoize', it => { +describe('argsMemoize and memoize', localTest => { beforeEach(context => { const store = setupStore() context.store = store + context.state = store.getState() }) - it('passing memoize directly to createSelector', ({ store }) => { + localTest('passing memoize directly to createSelector', ({ store }) => { const state = store.getState() const selectorDefault = createSelector( - (state: TodoState) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { memoize: defaultMemoize } ) const selectorDefaultParametric = createSelector( - (state: TodoState, id: number) => id, - (state: TodoState) => state.todos, + [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id), { memoize: defaultMemoize } ) @@ -505,9 +458,9 @@ describe('argsMemoize and memoize', it => { selectorDefaultParametric(deepClone(state), 0) const selectorAutotrack = createSelector( - (state: TodoState) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), - { memoize: unstable_autotrackMemoize } + { memoize: autotrackMemoize } ) const outPutSelectorFields: (keyof OutputSelectorFields)[] = [ 'memoize', @@ -584,164 +537,201 @@ describe('argsMemoize and memoize', it => { expect(selectorAutotrack.lastResult()).toBeUndefined() expect(selectorDefault.recomputations()).toBe(0) expect(selectorAutotrack.recomputations()).toBe(0) - expect(selectorDefault(state)).toStrictEqual([0, 1]) - expect(selectorAutotrack(state)).toStrictEqual([0, 1]) + expect(selectorDefault(state)).toStrictEqual(selectorAutotrack(state)) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) - selectorDefault(deepClone(state)) + // flipping completed flag does not cause the autotrack memoizer to re-run. + store.dispatch(toggleCompleted(0)) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) const defaultSelectorLastResult1 = selectorDefault.lastResult() - selectorDefault(deepClone(state)) - const defaultSelectorLastResult2 = selectorDefault.lastResult() - selectorAutotrack(deepClone(state)) const autotrackSelectorLastResult1 = selectorAutotrack.lastResult() - store.dispatch(todoSlice.actions.toggleCompleted(0)) // flipping completed flag does not cause the autotrack memoizer to re-run. + store.dispatch(toggleCompleted(0)) + selectorDefault(store.getState()) selectorAutotrack(store.getState()) + const defaultSelectorLastResult2 = selectorDefault.lastResult() const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(3) expect(selectorAutotrack.recomputations()).toBe(1) + for (let i = 0; i < 10; i++) { + store.dispatch(toggleCompleted(0)) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) + } + expect(selectorDefault.recomputations()).toBe(13) + expect(selectorAutotrack.recomputations()).toBe(1) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) // Default memoize does not preserve referential equality but autotrack does. expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) - store.dispatch(todoSlice.actions.addTodo(2)) + store.dispatch( + addTodo({ + title: 'Figure out if plants are really plotting world domination.', + description: 'They may be.' + }) + ) selectorAutotrack(store.getState()) expect(selectorAutotrack.recomputations()).toBe(2) }) - it('passing argsMemoize directly to createSelector', ({ store }) => { - const state = store.getState() + localTest('passing argsMemoize directly to createSelector', ({ store }) => { const otherCreateSelector = createSelectorCreator({ memoize: microMemoize, argsMemoize: microMemoize }) const selectorDefault = otherCreateSelector( - (state: TodoState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - argsMemoizeOptions: { - equalityCheck: (a, b) => a === b, - resultEqualityCheck: (a, b) => a === b - }, - memoizeOptions: { - equalityCheck: (a, b) => a === b, - resultEqualityCheck: (a, b) => a === b - } - } + { memoize: defaultMemoize, argsMemoize: defaultMemoize } ) const selectorAutotrack = createSelector( - (state: TodoState) => state.todos, - todos => todos.map(({ id }) => id) + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + expect(selectorDefault(store.getState())).toStrictEqual( + selectorAutotrack(store.getState()) ) - expect(selectorDefault({ ...state })).toStrictEqual([0, 1]) - expect(selectorAutotrack({ ...state })).toStrictEqual([0, 1]) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) - selectorDefault({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) - selectorDefault(state) - selectorDefault(state) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) + // toggling the completed flag should force the default memoizer to recalculate but not autotrack. + store.dispatch(toggleCompleted(0)) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(1)) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(2)) + selectorAutotrack(store.getState()) + selectorAutotrack(store.getState()) + selectorAutotrack(store.getState()) + selectorDefault(store.getState()) + selectorDefault(store.getState()) + selectorDefault(store.getState()) + store.dispatch(toggleCompleted(2)) + expect(selectorDefault.recomputations()).toBe(4) + expect(selectorAutotrack.recomputations()).toBe(1) + selectorDefault(store.getState()) + selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(0)) const defaultSelectorLastResult1 = selectorDefault.lastResult() - selectorDefault({ - todos: [ - { id: 0, completed: true }, - { id: 1, completed: true } - ] - }) + selectorDefault(store.getState()) + store.dispatch(toggleCompleted(0)) const defaultSelectorLastResult2 = selectorDefault.lastResult() - selectorAutotrack({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) + selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(0)) const autotrackSelectorLastResult1 = selectorAutotrack.lastResult() - selectorAutotrack({ - todos: [ - { id: 0, completed: true }, - { id: 1, completed: true } - ] - }) + selectorAutotrack(store.getState()) + store.dispatch(toggleCompleted(0)) const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() - expect(selectorDefault.recomputations()).toBe(4) - expect(selectorAutotrack.recomputations()).toBe(3) - expect(autotrackSelectorLastResult1).not.toBe(autotrackSelectorLastResult2) + expect(selectorDefault.recomputations()).toBe(6) + expect(selectorAutotrack.recomputations()).toBe(1) + expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) - + for (let i = 0; i < 10; i++) { + store.dispatch(toggleCompleted(0)) + selectorAutotrack(store.getState()) + } + for (let i = 0; i < 10; i++) { + store.dispatch(toggleCompleted(0)) + selectorDefault(store.getState()) + } + expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.recomputations()).toBe(16) // original options untouched. const selectorOriginal = createSelector( - (state: TodoState) => state.todos, - todos => todos.map(({ id }) => id), - { - memoizeOptions: { resultEqualityCheck: (a, b) => a === b } - } + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) ) - selectorOriginal(state) + selectorOriginal(store.getState()) const start = performance.now() for (let i = 0; i < 1_000_000_0; i++) { - selectorOriginal(state) + selectorOriginal(store.getState()) } const totalTime = performance.now() - start expect(totalTime).toBeLessThan(1000) - // Call with new reference to force the selector to re-run - selectorOriginal(deepClone(state)) - selectorOriginal(deepClone(state)) - // Override `argsMemoize` with `unstable_autotrackMemoize` + selectorOriginal(store.getState()) + // Override `argsMemoize` with `autotrackMemoize` const selectorOverrideArgsMemoize = createSelector( - (state: TodoState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { memoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a, b) => a === b }, - // WARNING!! This is just for testing purposes, do not use `unstable_autotrackMemoize` to memoize the arguments, + // WARNING!! This is just for testing purposes, do not use `autotrackMemoize` to memoize the arguments, // it can return false positives, since it's not tracking a nested field. - argsMemoize: unstable_autotrackMemoize + argsMemoize: autotrackMemoize } ) - selectorOverrideArgsMemoize(state) - // Call with new reference to force the selector to re-run - selectorOverrideArgsMemoize(deepClone(state)) - selectorOverrideArgsMemoize(deepClone(state)) + selectorOverrideArgsMemoize(store.getState()) + for (let i = 0; i < 10; i++) { + store.dispatch(toggleCompleted(0)) + selectorOverrideArgsMemoize(store.getState()) + selectorOriginal(store.getState()) + } expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) - expect(selectorOriginal.recomputations()).toBe(3) + expect(selectorOriginal.recomputations()).toBe(11) const selectorDefaultParametric = createSelector( - (state: TodoState, id: number) => id, - (state: TodoState) => state.todos, + [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id) ) - selectorDefaultParametric(state, 1) - selectorDefaultParametric(state, 1) + selectorDefaultParametric(store.getState(), 1) + selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(1) - selectorDefaultParametric(state, 2) - selectorDefaultParametric(state, 1) + selectorDefaultParametric(store.getState(), 2) + selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(3) - selectorDefaultParametric(state, 2) + selectorDefaultParametric(store.getState(), 2) expect(selectorDefaultParametric.recomputations()).toBe(4) const selectorDefaultParametricArgsWeakMap = createSelector( - (state: TodoState, id: number) => id, - (state: TodoState) => state.todos, + [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: weakMapMemoize } ) - selectorDefaultParametricArgsWeakMap(state, 1) - selectorDefaultParametricArgsWeakMap(state, 1) + const selectorDefaultParametricWeakMap = createSelector( + [(state: RootState, id: number) => id, (state: RootState) => state.todos], + (id, todos) => todos.filter(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + selectorDefaultParametricArgsWeakMap(store.getState(), 1) + selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) - selectorDefaultParametricArgsWeakMap(state, 2) - selectorDefaultParametricArgsWeakMap(state, 1) + selectorDefaultParametricArgsWeakMap(store.getState(), 2) + selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) - selectorDefaultParametricArgsWeakMap(state, 2) + selectorDefaultParametricArgsWeakMap(store.getState(), 2) // If we call the selector with 1, then 2, then 1 and back to 2 again, // `defaultMemoize` will recompute a total of 4 times, // but weakMapMemoize will recompute only twice. expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) + for (let i = 0; i < 10; i++) { + selectorDefaultParametricArgsWeakMap(store.getState(), 1) + selectorDefaultParametricArgsWeakMap(store.getState(), 2) + selectorDefaultParametricArgsWeakMap(store.getState(), 3) + selectorDefaultParametricArgsWeakMap(store.getState(), 4) + selectorDefaultParametricArgsWeakMap(store.getState(), 5) + } + expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(5) + for (let i = 0; i < 10; i++) { + selectorDefaultParametric(store.getState(), 1) + selectorDefaultParametric(store.getState(), 2) + selectorDefaultParametric(store.getState(), 3) + selectorDefaultParametric(store.getState(), 4) + selectorDefaultParametric(store.getState(), 5) + } + expect(selectorDefaultParametric.recomputations()).toBe(54) + for (let i = 0; i < 10; i++) { + selectorDefaultParametricWeakMap(store.getState(), 1) + selectorDefaultParametricWeakMap(store.getState(), 2) + selectorDefaultParametricWeakMap(store.getState(), 3) + selectorDefaultParametricWeakMap(store.getState(), 4) + selectorDefaultParametricWeakMap(store.getState(), 5) + } + expect(selectorDefaultParametricWeakMap.recomputations()).toBe(5) }) - it('passing argsMemoize to createSelectorCreator', ({ store }) => { + localTest('passing argsMemoize to createSelectorCreator', ({ store }) => { const state = store.getState() const createSelectorMicroMemoize = createSelectorCreator({ memoize: microMemoize, @@ -750,7 +740,7 @@ describe('argsMemoize and memoize', it => { argsMemoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicroMemoize = createSelectorMicroMemoize( - (state: TodoState) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id) ) expect(selectorMicroMemoize(state)).to.be.an('array').that.is.not.empty @@ -775,24 +765,33 @@ describe('argsMemoize and memoize', it => { expect(selectorMicroMemoize.lastResult()).to.be.an('array').that.is.not .empty expect( - selectorMicroMemoize.memoizedResultFunc([{ id: 0, completed: true }]) + selectorMicroMemoize.memoizedResultFunc([ + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } + ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoize.recomputations()).to.be.a('number') expect(selectorMicroMemoize.resetRecomputations()).toBe(0) expect(selectorMicroMemoize.resultFunc).to.be.a('function') expect( - selectorMicroMemoize.resultFunc([{ id: 0, completed: true }]) + selectorMicroMemoize.resultFunc([ + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } + ]) ).to.be.an('array').that.is.not.empty const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: TodoState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, - argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } - } + { memoize: defaultMemoize, argsMemoize: defaultMemoize } ) expect(selectorMicroMemoizeOverridden(state)).to.be.an('array').that.is.not .empty @@ -831,18 +830,30 @@ describe('argsMemoize and memoize', it => { .is.not.empty expect( selectorMicroMemoizeOverridden.memoizedResultFunc([ - { id: 0, completed: true } + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoizeOverridden.recomputations()).to.be.a('number') expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBe(0) expect( - selectorMicroMemoizeOverridden.resultFunc([{ id: 0, completed: true }]) + selectorMicroMemoizeOverridden.resultFunc([ + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } + ]) ).to.be.an('array').that.is.not.empty const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( - (state: TodoState) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, @@ -892,7 +903,12 @@ describe('argsMemoize and memoize', it => { ).that.is.not.empty expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc([ - { id: 0, completed: true } + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } ]) ).to.be.an('array').that.is.not.empty expect( @@ -903,17 +919,19 @@ describe('argsMemoize and memoize', it => { ).toBe(0) expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ - { id: 0, completed: true } + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } ]) ).to.be.an('array').that.is.not.empty const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: TodoState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), - { - memoize: defaultMemoize, - memoizeOptions: { resultEqualityCheck: (a, b) => a === b } - } + { memoize: defaultMemoize } ) expect(selectorMicroMemoizeOverrideMemoizeOnly(state)).to.be.an('array') .that.is.not.empty @@ -955,7 +973,12 @@ describe('argsMemoize and memoize', it => { ).that.is.not.empty expect( selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc([ - { id: 0, completed: true } + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoizeOverrideMemoizeOnly.recomputations()).to.be.a( @@ -966,20 +989,32 @@ describe('argsMemoize and memoize', it => { ) expect( selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ - { id: 0, completed: true } + { + id: 0, + completed: true, + title: 'Practice telekinesis for 15 minutes', + description: 'Just do it' + } ]) ).to.be.an('array').that.is.not.empty }) - it('pass options object to createSelectorCreator ', ({ store }) => { - const state = store.getState() + localTest('pass options object to createSelectorCreator ', ({ store }) => { const createSelectorMicro = createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicro = createSelectorMicro( - (state: TodoState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id) ) + expect(() => + //@ts-expect-error + createSelectorMicro([(state: RootState) => state.todos], 'a') + ).toThrowError( + TypeError( + `createSelector expects an output function after the inputs, but received: [string]` + ) + ) }) }) diff --git a/test/testUtils.ts b/test/testUtils.ts new file mode 100644 index 000000000..608a2d988 --- /dev/null +++ b/test/testUtils.ts @@ -0,0 +1,196 @@ +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' + +interface Todo { + id: number + title: string + description: string + completed: boolean +} + +interface Alert { + id: number + message: string + type: string + read: boolean +} + +const todoState = [ + { + id: 0, + title: 'Buy groceries', + description: 'Milk, bread, eggs, and fruits', + completed: false + }, + { + id: 1, + title: 'Schedule dentist appointment', + description: 'Check available slots for next week', + completed: false + }, + { + id: 2, + title: 'Convince the cat to get a job', + description: 'Need extra income for cat treats', + completed: false + }, + { + id: 3, + title: 'Figure out if plants are plotting world domination', + description: 'That cactus looks suspicious...', + completed: false + }, + { + id: 4, + title: 'Practice telekinesis', + description: 'Try moving the remote without getting up', + completed: false + }, + { + id: 5, + title: 'Determine location of El Dorado', + description: 'Might need it for the next vacation', + completed: false + }, + { + id: 6, + title: 'Master the art of invisible potato juggling', + description: 'Great party trick', + completed: false + } +] + +const alertState = [ + { + id: 0, + message: 'You have an upcoming meeting at 3 PM.', + type: 'reminder', + read: false + }, + { + id: 1, + message: 'New software update available.', + type: 'notification', + read: false + }, + { + id: 3, + message: + 'The plants have been watered, but keep an eye on that shifty cactus.', + type: 'notification', + read: false + }, + { + id: 4, + message: + 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', + type: 'reminder', + read: false + }, + { + id: 5, + message: + 'Expedition to El Dorado is postponed. The treasure map is being updated.', + type: 'notification', + read: false + }, + { + id: 6, + message: + 'Invisible potato juggling championship is tonight. May the best mime win.', + type: 'reminder', + read: false + } +] + +const todoSlice = createSlice({ + name: 'todos', + initialState: todoState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed + } + }, + + addTodo: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId, + completed: false + }) + }, + + removeTodo: (state, action: PayloadAction) => { + return state.filter(todo => todo.id !== action.payload) + }, + + updateTodo: (state, action: PayloadAction) => { + const index = state.findIndex(todo => todo.id === action.payload.id) + if (index !== -1) { + state[index] = action.payload + } + }, + + clearCompleted: state => { + return state.filter(todo => !todo.completed) + } + } +}) + +const alertSlice = createSlice({ + name: 'alerts', + initialState: alertState, + reducers: { + markAsRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = true + } + }, + + addAlert: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId + }) + }, + + removeAlert: (state, action: PayloadAction) => { + return state.filter(alert => alert.id !== action.payload) + } + } +}) + +const rootReducer = combineReducers({ + [todoSlice.name]: todoSlice.reducer, + [alertSlice.name]: alertSlice.reducer +}) + +export const setupStore = () => configureStore({ reducer: rootReducer }) + +export type AppStore = ReturnType + +export type RootState = ReturnType + +export interface LocalTestContext { + store: AppStore + state: RootState +} + +export const { markAsRead, addAlert, removeAlert } = alertSlice.actions + +export const { + toggleCompleted, + addTodo, + removeTodo, + updateTodo, + clearCompleted +} = todoSlice.actions + +// Since Node 16 does not support `structuredClone` +export const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) diff --git a/test/tsconfig.json b/test/tsconfig.json index be43155cb..dc51ba69e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -11,6 +11,7 @@ "target": "esnext", "jsx": "react", "baseUrl": ".", + "rootDir": ".", "skipLibCheck": true, "noImplicitReturns": false, "noUnusedLocals": false, diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 08211a839..acb9b1df0 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -1,10 +1,10 @@ import memoizeOne from 'memoize-one' import microMemoize from 'micro-memoize' import { - unstable_autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' import { expectExactType } from './test' @@ -46,26 +46,26 @@ function overrideOnlyMemoizeInCreateSelector() { const selectorAutotrackSeparateInlineArgs = createSelector( (state: State) => state.todos, todos => todos.map(t => t.id), - { memoize: unstable_autotrackMemoize } + { memoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( [(state: State) => state.todos], todos => todos.map(t => t.id), - { memoize: unstable_autotrackMemoize } + { memoize: autotrackMemoize } ) - // @ts-expect-error When memoize is unstable_autotrackMemoize, type of memoizeOptions needs to be the same as options args in unstable_autotrackMemoize. + // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), - { memoize: unstable_autotrackMemoize, memoizeOptions: { maxSize: 2 } } + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) - // @ts-expect-error When memoize is unstable_autotrackMemoize, type of memoizeOptions needs to be the same as options args in unstable_autotrackMemoize. + // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), - { memoize: unstable_autotrackMemoize, memoizeOptions: { maxSize: 2 } } + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorWeakMapSeparateInlineArgs = createSelector( (state: State) => state.todos, @@ -93,9 +93,7 @@ function overrideOnlyMemoizeInCreateSelector() { ) const createSelectorDefault = createSelectorCreator(defaultMemoize) const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) - const createSelectorAutotrack = createSelectorCreator( - unstable_autotrackMemoize - ) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( (state: State) => state.todos, todos => todos.map(t => t.id), @@ -112,7 +110,7 @@ function overrideOnlyMemoizeInCreateSelector() { { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = - // @ts-expect-error When memoize is changed to weakMapMemoize or unstable_autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. + // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( (state: State) => state.todos, // @ts-expect-error @@ -157,30 +155,30 @@ function overrideOnlyArgsMemoizeInCreateSelector() { const selectorAutotrackSeparateInlineArgs = createSelector( (state: State) => state.todos, todos => todos.map(t => t.id), - { argsMemoize: unstable_autotrackMemoize } + { argsMemoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( [(state: State) => state.todos], todos => todos.map(t => t.id), - { argsMemoize: unstable_autotrackMemoize } + { argsMemoize: autotrackMemoize } ) - // @ts-expect-error When argsMemoize is unstable_autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in unstable_autotrackMemoize. + // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { - argsMemoize: unstable_autotrackMemoize, + argsMemoize: autotrackMemoize, argsMemoizeOptions: { maxSize: 2 } } ) - // @ts-expect-error When argsMemoize is unstable_autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in unstable_autotrackMemoize. + // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { - argsMemoize: unstable_autotrackMemoize, + argsMemoize: autotrackMemoize, argsMemoizeOptions: { maxSize: 2 } } ) @@ -210,9 +208,7 @@ function overrideOnlyArgsMemoizeInCreateSelector() { ) const createSelectorDefault = createSelectorCreator(defaultMemoize) const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) - const createSelectorAutotrack = createSelectorCreator( - unstable_autotrackMemoize - ) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( (state: State) => state.todos, todos => todos.map(t => t.id), @@ -229,7 +225,7 @@ function overrideOnlyArgsMemoizeInCreateSelector() { { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = - // @ts-expect-error When argsMemoize is changed to weakMapMemoize or unstable_autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. + // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( (state: State) => state.todos, // @ts-expect-error @@ -421,7 +417,20 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { expectExactType( selectorMicroMemoizeOverriddenArray.resultFunc([{ id: 0, completed: true }]) ) - // Making sure the type of `memoizeOptions` remains the same when only overriding `argsMemoize` + const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = + // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. + createSelectorMicroMemoize( + (state: State) => state.todos, + todos => todos.map(({ id }) => id), + { + argsMemoize: defaultMemoize, + memoizeOptions: { + isPromise: false, + resultEqualityCheck: (a: unknown, b: unknown) => a === b + }, + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( (state: State) => state.todos, @@ -656,6 +665,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { ) // @ts-expect-error selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.clearCache() // @ts-expect-error selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.cache @@ -703,6 +713,12 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { { id: 0, completed: true } ]) ) + expectExactType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoize + ) + expectExactType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.argsMemoize + ) const createSelectorWithWrongArgsMemoizeOptions = // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `defaultMemoize`. @@ -726,3 +742,169 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { [] // This causes the error. ) } + +function deepNesting() { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector(selector0, s => s) + const selector2 = createSelector(selector1, s => s) + const selector3 = createSelector(selector2, s => s) + const selector4 = createSelector(selector3, s => s) + const selector5 = createSelector(selector4, s => s) + const selector6 = createSelector(selector5, s => s) + const selector7 = createSelector(selector6, s => s) + const selector8 = createSelector(selector7, s => s) + const selector9 = createSelector(selector8, s => s) + const selector10 = createSelector(selector9, s => s, { + memoize: microMemoize + }) + selector10.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache + const selector11 = createSelector(selector10, s => s) + const selector12 = createSelector(selector11, s => s) + const selector13 = createSelector(selector12, s => s) + const selector14 = createSelector(selector13, s => s) + const selector15 = createSelector(selector14, s => s) + const selector16 = createSelector(selector15, s => s) + const selector17 = createSelector(selector16, s => s) + const selector18 = createSelector(selector17, s => s) + const selector19 = createSelector(selector18, s => s) + const selector20 = createSelector(selector19, s => s) + selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.cache +} + +function deepNesting1() { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector([selector0], s => s) + const selector2 = createSelector([selector1], s => s) + const selector3 = createSelector([selector2], s => s) + const selector4 = createSelector([selector3], s => s) + const selector5 = createSelector([selector4], s => s) + const selector6 = createSelector([selector5], s => s) + const selector7 = createSelector([selector6], s => s) + const selector8 = createSelector([selector7], s => s) + const selector9 = createSelector([selector8], s => s) + const selector10 = createSelector([selector9], s => s) + const selector11 = createSelector([selector10], s => s) + const selector12 = createSelector([selector11], s => s) + const selector13 = createSelector([selector12], s => s) + const selector14 = createSelector([selector13], s => s) + const selector15 = createSelector([selector14], s => s) + const selector16 = createSelector([selector15], s => s) + const selector17 = createSelector([selector16], s => s) + const selector18 = createSelector([selector17], s => s) + const selector19 = createSelector([selector18], s => s) + const selector20 = createSelector([selector19], s => s) + const selector21 = createSelector([selector20], s => s) + const selector22 = createSelector([selector21], s => s) + const selector23 = createSelector([selector22], s => s) + const selector24 = createSelector([selector23], s => s) + const selector25 = createSelector([selector24], s => s) + const selector26 = createSelector([selector25], s => s) + const selector27 = createSelector([selector26], s => s) + const selector28 = createSelector([selector27], s => s) + const selector29 = createSelector([selector28], s => s) + const selector30 = createSelector([selector29], s => s) +} + +function deepNesting2() { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector(selector0, s => s, { + memoize: defaultMemoize + }) + const selector2 = createSelector(selector1, s => s, { + memoize: defaultMemoize + }) + const selector3 = createSelector(selector2, s => s, { + memoize: defaultMemoize + }) + const selector4 = createSelector(selector3, s => s, { + memoize: defaultMemoize + }) + const selector5 = createSelector(selector4, s => s, { + memoize: defaultMemoize + }) + const selector6 = createSelector(selector5, s => s, { + memoize: defaultMemoize + }) + const selector7 = createSelector(selector6, s => s, { + memoize: defaultMemoize + }) + const selector8 = createSelector(selector7, s => s, { + memoize: defaultMemoize + }) + const selector9 = createSelector(selector8, s => s, { + memoize: defaultMemoize + }) + const selector10 = createSelector(selector9, s => s, { + memoize: defaultMemoize + }) + const selector11 = createSelector(selector10, s => s, { + memoize: defaultMemoize + }) + const selector12 = createSelector(selector11, s => s, { + memoize: defaultMemoize + }) + const selector13 = createSelector(selector12, s => s, { + memoize: defaultMemoize + }) + const selector14 = createSelector(selector13, s => s, { + memoize: defaultMemoize + }) + const selector15 = createSelector(selector14, s => s, { + memoize: defaultMemoize + }) + const selector16 = createSelector(selector15, s => s, { + memoize: defaultMemoize + }) + const selector17 = createSelector(selector16, s => s, { + memoize: defaultMemoize + }) + const selector18 = createSelector(selector17, s => s, { + memoize: defaultMemoize + }) + const selector19 = createSelector(selector18, s => s, { + memoize: defaultMemoize + }) + const selector20 = createSelector(selector19, s => s, { + memoize: defaultMemoize + }) + const selector21 = createSelector(selector20, s => s, { + memoize: defaultMemoize + }) + const selector22 = createSelector(selector21, s => s, { + memoize: defaultMemoize + }) + const selector23 = createSelector(selector22, s => s, { + memoize: defaultMemoize + }) + const selector24 = createSelector(selector23, s => s, { + memoize: defaultMemoize + }) + const selector25 = createSelector(selector24, s => s, { + memoize: defaultMemoize + }) + const selector26 = createSelector(selector25, s => s, { + memoize: defaultMemoize + }) + const selector27 = createSelector(selector26, s => s, { + memoize: defaultMemoize + }) + const selector28 = createSelector(selector27, s => s, { + memoize: defaultMemoize + }) + const selector29 = createSelector(selector28, s => s, { + memoize: defaultMemoize + }) +} diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 5f9187a87..8b4b85dd6 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -1,5 +1,6 @@ /* eslint-disable no-use-before-define */ +import type { AnyFunction, ExtractMemoizerFields } from '@internal/types' import { configureStore, createSlice } from '@reduxjs/toolkit' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { groupBy, isEqual } from 'lodash' @@ -7,12 +8,13 @@ import memoizeOne from 'memoize-one' import microMemoize from 'micro-memoize' import type { TypedUseSelectorHook } from 'react-redux' import { useSelector } from 'react-redux' -import { +import type { GetStateFromSelectors, - OutputSelector, ParametricSelector, - Selector, SelectorResultArray, + TypedStructuredSelectorCreator +} from 'reselect' +import { createSelector, createSelectorCreator, createStructuredSelector, @@ -764,18 +766,20 @@ function testCreateStructuredSelector() { bar: (state: StateAB, c: number, d: string) => state.b }) - const selector = createStructuredSelector< - { foo: string }, - { - foo: string - bar: number - } - >({ + interface RootState { + foo: string + bar: number + } + + const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = + createStructuredSelector + + const selector = typedStructuredSelectorCreator({ foo: state => state.foo, bar: state => +state.foo }) - const res1 = selector({ foo: '42' }) + const res1 = selector({ foo: '42', bar: 1 }) const foo: string = res1.foo const bar: number = res1.bar @@ -785,17 +789,17 @@ function testCreateStructuredSelector() { // @ts-expect-error selector({ foo: '42' }, { bar: 42 }) - createStructuredSelector<{ foo: string }, { bar: number }>({ + typedStructuredSelectorCreator({ // @ts-expect-error bar: (state: { baz: boolean }) => 1 }) - createStructuredSelector<{ foo: string }, { bar: number }>({ + typedStructuredSelectorCreator({ // @ts-expect-error bar: state => state.foo }) - createStructuredSelector<{ foo: string }, { bar: number }>({ + typedStructuredSelectorCreator({ // @ts-expect-error baz: state => state.foo }) @@ -825,7 +829,7 @@ function testCreateStructuredSelector() { const resOneParam = oneParamSelector({ a: 1, b: 2 }) const resThreeParams = threeParamSelector({ a: 1, b: 2 }, 99, 'blah') - const res2: ExpectedResult = selector({ foo: '42' }) + const res2: ExpectedResult = selector({ foo: '42', bar: 0 }) const res3: ExpectedResult = selector2({ foo: '42' }, 99, 'test') const resGenerics: ExpectedResult = selectorGenerics( { foo: '42' }, @@ -839,6 +843,50 @@ function testCreateStructuredSelector() { selectorGenerics({ bar: '42' }) } +function testTypedCreateStructuredSelector() { + type RootState = { + foo: string + bar: number + } + + const selectFoo = (state: RootState) => state.foo + const selectBar = (state: RootState) => state.bar + + const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = + createStructuredSelector as TypedStructuredSelectorCreator + + typedStructuredSelectorCreator({ + foo: selectFoo, + bar: selectBar + }) + + // @ts-expect-error + typedStructuredSelectorCreator({ + foo: selectFoo + }) + + // This works + const selectorGenerics = createStructuredSelector<{ + foo: typeof selectFoo + bar: typeof selectBar + }>({ + foo: state => state.foo, + bar: state => +state.foo + }) + + // This also works + const selectorGenerics1 = typedStructuredSelectorCreator<{ + foo: typeof selectFoo + bar: typeof selectBar + }>({ + foo: state => state.foo, + bar: state => +state.foo + }) + + // Their types are the same. + expectExactType(selectorGenerics) +} + function testDynamicArrayArgument() { interface Elem { val1: string @@ -897,19 +945,22 @@ function testStructuredSelectorTypeParams() { // ^^^ because this is missing, an error is thrown }) + const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = + createStructuredSelector + // This works - createStructuredSelector({ + typedStructuredSelectorCreator({ foo: selectFoo, bar: selectBar }) - // So does this - createStructuredSelector>({ - foo: selectFoo - }) + // // So does this + // typedStructuredSelectorCreator>({ + // foo: selectFoo + // }) } -function multiArgMemoize any>( +function multiArgMemoize( func: F, a: number, b: string, @@ -1332,10 +1383,7 @@ function deepNesting() { const selector5 = createSelector(selector4, s => s) const selector6 = createSelector(selector5, s => s) const selector7 = createSelector(selector6, s => s) - const selector8: Selector = createSelector( - selector7, - s => s - ) + const selector8 = createSelector(selector7, s => s) const selector9 = createSelector(selector8, s => s) const selector10 = createSelector(selector9, s => s) const selector11 = createSelector(selector10, s => s) @@ -1344,10 +1392,7 @@ function deepNesting() { const selector14 = createSelector(selector13, s => s) const selector15 = createSelector(selector14, s => s) const selector16 = createSelector(selector15, s => s) - const selector17: OutputSelector< - [(state: State) => string], - ReturnType - > = createSelector(selector16, s => s) + const selector17 = createSelector(selector16, s => s) const selector18 = createSelector(selector17, s => s) const selector19 = createSelector(selector18, s => s) const selector20 = createSelector(selector19, s => s) @@ -1356,10 +1401,7 @@ function deepNesting() { const selector23 = createSelector(selector22, s => s) const selector24 = createSelector(selector23, s => s) const selector25 = createSelector(selector24, s => s) - const selector26: Selector< - typeof selector25 extends Selector ? S : never, - ReturnType - > = createSelector(selector25, s => s) + const selector26 = createSelector(selector25, s => s) const selector27 = createSelector(selector26, s => s) const selector28 = createSelector(selector27, s => s) const selector29 = createSelector(selector28, s => s) @@ -1662,3 +1704,78 @@ function issue555() { const selectorResult2 = someSelector2(state, undefined) const selectorResult3 = someSelector3(state, null) } + +function testCreateStructuredSelectorNew() { + interface State { + todos: { + id: number + completed: boolean + }[] + } + const state: State = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] + } + + const selectorDefaultParametric = createSelector( + (state: State, id: number) => id, + (state: State) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id) + ) + const multiArgsStructuredSelector = createStructuredSelector( + { + selectedTodos: (state: State) => state.todos, + selectedTodoById: (state: State, id: number) => state.todos[id], + selectedCompletedTodos: ( + state: State, + id: number, + isCompleted: boolean + ) => state.todos.filter(({ completed }) => completed === isCompleted) + }, + createSelectorCreator({ memoize: microMemoize, argsMemoize: microMemoize }) + ) + + multiArgsStructuredSelector.resultFunc( + [{ id: 2, completed: true }], + { id: 0, completed: false }, + [{ id: 0, completed: false }] + ).selectedCompletedTodos + + multiArgsStructuredSelector.memoizedResultFunc( + [{ id: 2, completed: true }], + { id: 0, completed: false }, + [{ id: 0, completed: false }] + ).selectedCompletedTodos + + multiArgsStructuredSelector.memoizedResultFunc.cache + multiArgsStructuredSelector.memoizedResultFunc.fn + multiArgsStructuredSelector.memoizedResultFunc.isMemoized + multiArgsStructuredSelector.memoizedResultFunc.options + + multiArgsStructuredSelector(state, 2, true).selectedCompletedTodos + expectExactType(multiArgsStructuredSelector.argsMemoize) + expectExactType(multiArgsStructuredSelector.memoize) + expectExactType['cache']>( + multiArgsStructuredSelector.cache + ) + expectExactType['fn']>( + multiArgsStructuredSelector.fn + ) + expectExactType['isMemoized']>( + multiArgsStructuredSelector.isMemoized + ) + expectExactType['options']>( + multiArgsStructuredSelector.options + ) + expectExactType< + [ + (state: State) => State['todos'], + (state: State, id: number) => State['todos'][number], + (state: State, id: number, isCompleted: boolean) => State['todos'] + ] + >(multiArgsStructuredSelector.dependencies) + // @ts-expect-error Wrong number of arguments. + multiArgsStructuredSelector(state, 2) +} From a1cf7e357c4aad6ef7978fa832a4c8a95aa71dd1 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 11:58:28 -0500 Subject: [PATCH 3/5] Setup benchmarks with vitest --- package.json | 1 + test/reselect.bench.ts | 199 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 181 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 69fc6b378..8ca716863 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"", "lint": "eslint src test", "prepack": "yarn build", + "bench": "vitest --run bench", "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run", "test:cov": "vitest run --coverage", "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts index 06f979001..419ee1562 100644 --- a/test/reselect.bench.ts +++ b/test/reselect.bench.ts @@ -3,6 +3,11 @@ import { bench } from 'vitest' import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' import { weakMapMemoize } from '../src/weakMapMemoize' +const options: NonNullable[2]> = { + iterations: 1_000_000, + time: 100 +} + describe('bench', () => { interface State { todos: { @@ -13,44 +18,200 @@ describe('bench', () => { const state: State = { todos: [ { id: 0, completed: false }, - { id: 1, completed: false } + { id: 1, completed: false }, + { id: 2, completed: false }, + { id: 3, completed: false }, + { id: 4, completed: false }, + { id: 5, completed: false }, + { id: 6, completed: false }, + { id: 7, completed: false }, + { id: 8, completed: false }, + { id: 9, completed: false }, + { id: 10, completed: false }, + { id: 11, completed: false }, + { id: 12, completed: false }, + { id: 13, completed: false }, + { id: 14, completed: false }, + { id: 15, completed: false }, + { id: 16, completed: false }, + { id: 17, completed: false }, + { id: 18, completed: false }, + { id: 19, completed: false }, + { id: 20, completed: false }, + { id: 21, completed: false }, + { id: 22, completed: false }, + { id: 23, completed: false }, + { id: 24, completed: false }, + { id: 25, completed: false }, + { id: 26, completed: false }, + { id: 27, completed: false }, + { id: 28, completed: false }, + { id: 29, completed: false }, + { id: 30, completed: false }, + { id: 31, completed: false }, + { id: 32, completed: false }, + { id: 33, completed: false }, + { id: 34, completed: false }, + { id: 35, completed: false }, + { id: 36, completed: false }, + { id: 37, completed: false }, + { id: 38, completed: false }, + { id: 39, completed: false }, + { id: 40, completed: false }, + { id: 41, completed: false }, + { id: 42, completed: false }, + { id: 43, completed: false }, + { id: 44, completed: false }, + { id: 45, completed: false }, + { id: 46, completed: false }, + { id: 47, completed: false }, + { id: 48, completed: false }, + { id: 49, completed: false }, + { id: 50, completed: false }, + { id: 51, completed: false }, + { id: 52, completed: false }, + { id: 53, completed: false }, + { id: 54, completed: false }, + { id: 55, completed: false }, + { id: 56, completed: false }, + { id: 57, completed: false }, + { id: 58, completed: false }, + { id: 59, completed: false }, + { id: 60, completed: false }, + { id: 61, completed: false }, + { id: 62, completed: false }, + { id: 63, completed: false }, + { id: 64, completed: false }, + { id: 65, completed: false }, + { id: 66, completed: false }, + { id: 67, completed: false }, + { id: 68, completed: false }, + { id: 69, completed: false }, + { id: 70, completed: false }, + { id: 71, completed: false }, + { id: 72, completed: false }, + { id: 73, completed: false }, + { id: 74, completed: false }, + { id: 75, completed: false }, + { id: 76, completed: false }, + { id: 77, completed: false }, + { id: 78, completed: false }, + { id: 79, completed: false }, + { id: 80, completed: false }, + { id: 81, completed: false }, + { id: 82, completed: false }, + { id: 83, completed: false }, + { id: 84, completed: false }, + { id: 85, completed: false }, + { id: 86, completed: false }, + { id: 87, completed: false }, + { id: 88, completed: false }, + { id: 89, completed: false }, + { id: 90, completed: false }, + { id: 91, completed: false }, + { id: 92, completed: false }, + { id: 93, completed: false }, + { id: 94, completed: false }, + { id: 95, completed: false }, + { id: 96, completed: false }, + { id: 97, completed: false }, + { id: 98, completed: false }, + { id: 99, completed: false } ] } + const selectorDefault = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id) + ) + const selectorAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) + const selectorArgsWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id], + { + argsMemoize: weakMapMemoize + } + ) bench( 'selectorDefault', () => { - const selectorDefault = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) selectorDefault(state) }, - { iterations: 500 } + options ) bench( 'selectorAutotrack', () => { - const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) selectorAutotrack(state) }, - { iterations: 500 } + options ) - bench( 'selectorWeakMap', () => { - const selectorWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) selectorWeakMap(state) }, - { iterations: 500 } + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options ) }) From d7b33b592379542b17ff0f4dc71fd460f1996e02 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 13:31:39 -0500 Subject: [PATCH 4/5] Remove internal type imports in TS test files --- typescript_test/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 8b4b85dd6..f07f78dc7 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -1,6 +1,6 @@ /* eslint-disable no-use-before-define */ -import type { AnyFunction, ExtractMemoizerFields } from '@internal/types' +import type { AnyFunction, ExtractMemoizerFields } from '../src/types' import { configureStore, createSlice } from '@reduxjs/toolkit' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { groupBy, isEqual } from 'lodash' From 26149932ea9db419c823319b355b06e20e7063f6 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 13:55:40 -0500 Subject: [PATCH 5/5] Remove internal type imports in TS test files --- typescript_test/test.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/typescript_test/test.ts b/typescript_test/test.ts index f07f78dc7..c6be7e511 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -1,6 +1,5 @@ /* eslint-disable no-use-before-define */ -import type { AnyFunction, ExtractMemoizerFields } from '../src/types' import { configureStore, createSlice } from '@reduxjs/toolkit' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { groupBy, isEqual } from 'lodash' @@ -960,7 +959,7 @@ function testStructuredSelectorTypeParams() { // }) } -function multiArgMemoize( +function multiArgMemoize any>( func: F, a: number, b: string, @@ -1757,18 +1756,6 @@ function testCreateStructuredSelectorNew() { multiArgsStructuredSelector(state, 2, true).selectedCompletedTodos expectExactType(multiArgsStructuredSelector.argsMemoize) expectExactType(multiArgsStructuredSelector.memoize) - expectExactType['cache']>( - multiArgsStructuredSelector.cache - ) - expectExactType['fn']>( - multiArgsStructuredSelector.fn - ) - expectExactType['isMemoized']>( - multiArgsStructuredSelector.isMemoized - ) - expectExactType['options']>( - multiArgsStructuredSelector.options - ) expectExactType< [ (state: State) => State['todos'],