diff --git a/.gitignore b/.gitignore index 9876dbe03..d288a6f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ typescript_test/common.js flow_test/should_fail/flow-typed/index.js.flow flow_test/should_pass/flow-typed/index.js.flow reselect-builds/ - +trace typesversions .cache @@ -24,4 +24,4 @@ typesversions !.yarn/sdks !.yarn/versions .pnp.* -*.tgz \ No newline at end of file +*.tgz diff --git a/package.json b/package.json index 563a8f3ef..ab8ae31a8 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"", "lint": "eslint src test", "prepack": "yarn build", - "bench": "vitest --run bench", + "bench": "vitest --run bench --mode production", "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run", "test:cov": "vitest run --coverage", + "type-check": "vitest --run typecheck", + "type-check:trace": "vitest --run typecheck && tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace", "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" }, "keywords": [ @@ -54,6 +56,7 @@ "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/eslint-plugin-tslint": "5.1.0", "@typescript-eslint/parser": "5.1.0", + "@typescript/analyze-trace": "^0.10.1", "eslint": "^8.0.1", "eslint-plugin-react": "^7.26.1", "eslint-plugin-typescript": "0.14.0", diff --git a/src/autotrackMemoize/autotrackMemoize.ts b/src/autotrackMemoize/autotrackMemoize.ts index 91113aa51..a09835bdf 100644 --- a/src/autotrackMemoize/autotrackMemoize.ts +++ b/src/autotrackMemoize/autotrackMemoize.ts @@ -5,7 +5,11 @@ import { createCacheKeyComparator, defaultEqualityCheck } from '@internal/defaultMemoize' -import type { AnyFunction } from '@internal/types' +import type { + AnyFunction, + DefaultMemoizeFields, + Simplify +} from '@internal/types' import { createCache } from './autotracking' /** @@ -55,7 +59,7 @@ import { createCache } from './autotracking' * ```ts * import { unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator } from 'reselect' * - * const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + * const createSelectorAutotrack = createSelectorCreator({ memoize: autotrackMemoize }) * * const selectTodoIds = createSelectorAutotrack( * [(state: RootState) => state.todos], @@ -95,7 +99,9 @@ export function autotrackMemoize(func: Func) { return cache.value } - memoized.clearCache = () => cache.clear() + memoized.clearCache = () => { + return cache.clear() + } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 3045472d9..5d083a964 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -1,4 +1,3 @@ -import type { OutputSelector, Selector, SelectorArray } from 'reselect' import { defaultMemoize } from './defaultMemoize' import type { @@ -9,6 +8,11 @@ import type { GetParamsFromSelectors, GetStateFromSelectors, InterruptRecursion, + OutputSelector, + Selector, + SelectorArray, + SetRequired, + Simplify, StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -82,7 +86,7 @@ export interface CreateSelectorFunction< ...createSelectorArgs: [ ...inputSelectors: InputSelectors, combiner: Combiner, - createSelectorOptions: Partial< + createSelectorOptions: Simplify< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -122,7 +126,7 @@ export interface CreateSelectorFunction< >( inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: Partial< + createSelectorOptions?: Simplify< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -220,11 +224,16 @@ export function createSelectorCreator< MemoizeFunction extends UnknownMemoizer, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - options: CreateSelectorOptions< - typeof defaultMemoize, - typeof defaultMemoize, - MemoizeFunction, - ArgsMemoizeFunction + options: Simplify< + SetRequired< + CreateSelectorOptions< + typeof defaultMemoize, + typeof defaultMemoize, + MemoizeFunction, + ArgsMemoizeFunction + >, + 'memoize' + > > ): CreateSelectorFunction @@ -276,27 +285,29 @@ export function createSelectorCreator< ArgsMemoizeFunction extends UnknownMemoizer, MemoizeOrOptions extends | MemoizeFunction - | CreateSelectorOptions + | SetRequired< + CreateSelectorOptions, + 'memoize' + > >( memoizeOrOptions: MemoizeOrOptions, - ...memoizeOptionsFromArgs: MemoizeOrOptions extends CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction + ...memoizeOptionsFromArgs: MemoizeOrOptions extends SetRequired< + CreateSelectorOptions, + 'memoize' > ? never : DropFirstParameter ) { /** options initially passed into `createSelectorCreator`. */ - const createSelectorCreatorOptions: CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction - > = - typeof memoizeOrOptions === 'function' - ? { - memoize: memoizeOrOptions as MemoizeFunction, - memoizeOptions: memoizeOptionsFromArgs - } - : memoizeOrOptions + const createSelectorCreatorOptions: SetRequired< + CreateSelectorOptions, + 'memoize' + > = typeof memoizeOrOptions === 'function' + ? { + memoize: memoizeOrOptions as MemoizeFunction, + memoizeOptions: memoizeOptionsFromArgs + } + : memoizeOrOptions const createSelector = < InputSelectors extends SelectorArray, @@ -307,41 +318,36 @@ export function createSelectorCreator< ...createSelectorArgs: [ ...inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + createSelectorOptions?: CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > ] ) => { let recomputations = 0 + let dependencyRecomputations = 0 let lastResult: Result - // Due to the intricacies of rest params, we can't do an optional arg after `...funcs`. + // Due to the intricacies of rest params, we can't do an optional arg after `...createSelectorArgs`. // So, start by declaring the default value here. // (And yes, the words 'memoize' and 'options' appear too many times in this next sequence.) - let directlyPassedOptions: Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + let directlyPassedOptions: CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > = {} // Normally, the result func or "combiner" is the last arg let resultFunc = createSelectorArgs.pop() as | Combiner - | Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + | CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > // If the result func is actually an _object_, assume it's our options object @@ -395,6 +401,7 @@ export function createSelectorCreator< // If a selector is called with the exact same arguments we don't need to traverse our dependencies again. const selector = argsMemoize(function dependenciesChecker() { + dependencyRecomputations++ /** Return values of input selectors which the `resultFunc` takes as arguments. */ const inputSelectorResults = collectInputSelectorResults( dependencies, @@ -436,6 +443,8 @@ export function createSelectorCreator< resultFunc, memoizedResultFunc, dependencies, + dependencyRecomputations: () => dependencyRecomputations, + resetDependencyRecomputations: () => (dependencyRecomputations = 0), lastResult: () => lastResult, recomputations: () => recomputations, resetRecomputations: () => (recomputations = 0), diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 3ca2ae2fc..a57897ea8 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -3,9 +3,11 @@ import { createSelector } from './createSelectorCreator' import type { CreateSelectorFunction } from './createSelectorCreator' import type { defaultMemoize } from './defaultMemoize' import type { + InterruptRecursion, ObjectValuesToTuple, OutputSelector, Selector, + Simplify, UnknownMemoizer } from './types' import { assertIsObject } from './utils' @@ -65,11 +67,10 @@ 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. + * A convenience function that simplifies returning an object + * made up of selector results. * - * @param selectorMap - A key value pair consisting of input selectors. + * @param inputSelectorsObject - A key value pair consisting of input selectors. * @param selectorCreator - A custom selector creator function. It defaults to `createSelector`. * @returns A memoized structured selector. * @@ -78,58 +79,22 @@ export interface StructuredSelectorCreator { * ```ts * import { createSelector, createStructuredSelector } from 'reselect' * - * interface State { + * interface RootState { * todos: { * id: number + * completed: boolean * 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 - * } - * ] + * alerts: { id: number; read: boolean }[] * } * * // This: * const structuredSelector = createStructuredSelector( * { - * allTodos: (state: State) => state.todos, - * allAlerts: (state: State) => state.alerts, - * selectedTodo: (state: State, id: number) => state.todos[id] + * todos: (state: RootState) => state.todos, + * alerts: (state: RootState) => state.alerts, + * todoById: (state: RootState, id: number) => state.todos[id] * }, * createSelector * ) @@ -137,15 +102,15 @@ export interface StructuredSelectorCreator { * // Is essentially the same as this: * const selector = createSelector( * [ - * (state: State) => state.todos, - * (state: State) => state.alerts, - * (state: State, id: number) => state.todos[id] + * (state: RootState) => state.todos, + * (state: RootState) => state.alerts, + * (state: RootState, id: number) => state.todos[id] * ], - * (allTodos, allAlerts, selectedTodo) => { + * (todos, alerts, todoById) => { * return { - * allTodos, - * allAlerts, - * selectedTodo + * todos, + * alerts, + * todoById * } * } * ) @@ -178,60 +143,64 @@ export interface StructuredSelectorCreator { MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - selectorMap: InputSelectorsObject, + inputSelectorsObject: InputSelectorsObject, selectorCreator?: CreateSelectorFunction< MemoizeFunction, ArgsMemoizeFunction > ): OutputSelector< ObjectValuesToTuple, - SelectorsMap, + Simplify>, MemoizeFunction, ArgsMemoizeFunction - > - // TODO: Do we need this? - /** - * Second overload - */ - // < - // 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 - // > + > & + InterruptRecursion } -// Manual definition of state and output arguments /** - * 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. + * A convenience function that simplifies returning an object + * made up of selector results. * * @example - * Simple Use Case + * Modern Use Case * ```ts - * const selectA = state => state.a - * const selectB = state => state.b + * import { createSelector, createStructuredSelector } from 'reselect' + * + * interface RootState { + * todos: { + * id: number + * completed: boolean + * title: string + * description: string + * }[] + * alerts: { id: number; read: boolean }[] + * } + * + * // This: + * const structuredSelector = createStructuredSelector( + * { + * todos: (state: RootState) => state.todos, + * alerts: (state: RootState) => state.alerts, + * todoById: (state: RootState, id: number) => state.todos[id] + * }, + * createSelector + * ) * - * // 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 - * })) + * // Is essentially the same as this: + * const selector = createSelector( + * [ + * (state: RootState) => state.todos, + * (state: RootState) => state.alerts, + * (state: RootState, id: number) => state.todos[id] + * ], + * (todos, alerts, todoById) => { + * return { + * todos, + * alerts, + * todoById + * } + * } + * ) * ``` * * @see {@link https://github.com/reduxjs/reselect#createstructuredselector-inputselectorsobject--selectorcreator--createselector createStructuredSelector} diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index a262e35c3..716c8a4df 100644 --- a/src/defaultMemoize.ts +++ b/src/defaultMemoize.ts @@ -1,4 +1,9 @@ -import type { AnyFunction, EqualityFn } from './types' +import type { + AnyFunction, + DefaultMemoizeFields, + EqualityFn, + Simplify +} from './types' // Cache implementation based on Erik Rasmussen's `lru-memoize`: // https://github.com/erikras/lru-memoize @@ -88,6 +93,9 @@ function createLruCache(maxSize: number, equals: EqualityFn): Cache { } /** + * Runs a simple reference equality check. + * What {@linkcode defaultMemoize defaultMemoize} uses by default. + * * @public */ export const defaultEqualityCheck: EqualityFn = (a, b): boolean => { @@ -206,5 +214,5 @@ export function defaultMemoize( cache.clear() } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/src/index.ts b/src/index.ts index c6db78efe..6a85cb051 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,16 +15,19 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, + DefaultMemoizeFields, EqualityFn, + ExtractMemoizerFields, GetParamsFromSelectors, GetStateFromSelectors, - OutputParametricSelector, + MemoizeOptionsFromParameters, OutputSelector, OutputSelectorFields, - ParametricSelector, + OverrideMemoizeOptions, Selector, SelectorArray, SelectorResultArray, - StabilityCheckFrequency + StabilityCheckFrequency, + UnknownMemoizer } from './types' export { weakMapMemoize } from './weakMapMemoize' diff --git a/src/types.ts b/src/types.ts index 775d9c370..e4bd854cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,36 +93,39 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos) => todos[id], + * (items, category) => items.filter(item => item.category === category), * { 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 + 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`). + * 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. + * 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( + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos) => todos[id], + * (items, category) => items.filter(item => item.category === category), * { argsMemoize: weakMapMemoize } * ) * ``` @@ -131,9 +134,6 @@ export interface CreateSelectorOptions< * * @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 @@ -145,7 +145,6 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ - // Should dynamically change to the options argument of `memoize`. memoizeOptions?: OverrideMemoizeOptions< MemoizeFunction, OverrideMemoizeFunction @@ -187,19 +186,55 @@ export type OutputSelectorFields< MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > = { - /** 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 OutputSelectorFields.resultFunc resultFunc}. */ + + /** + * The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. + */ memoizedResultFunc: Combiner & ExtractMemoizerFields - /** Returns the last result calculated by the output selector. */ + + /** + * @Returns The last result calculated by {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}. + */ lastResult: () => Result - /** An array of the input selectors. */ + + /** + * The array of the input selectors used by `createSelector` to compose the + * combiner ({@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}). + */ dependencies: InputSelectors - /** Counts the number of times the output has been recalculated. */ + + /** + * Counts the number of times {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc} has been recalculated. + */ recomputations: () => number - /** Resets the count of `recomputations` count to 0. */ - resetRecomputations: () => 0 + + /** + * Resets the count of {@linkcode OutputSelectorFields.recomputations recomputations} count to 0. + */ + resetRecomputations: () => void + + /** + * Counts the number of times the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) + * have been recalculated. This is distinct from {@linkcode OutputSelectorFields.recomputations recomputations}, + * which tracks the recalculations of the result function. + * + * @since 5.0.0 + */ + dependencyRecomputations: () => number + + /** + * Resets the count {@linkcode OutputSelectorFields.dependencyRecomputations dependencyRecomputations} + * for the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) + * of a memoized selector. + * + * @since 5.0.0 + */ + resetDependencyRecomputations: () => void } & Simplify< Required< Pick< @@ -255,30 +290,6 @@ export type Combiner = Distribute< (...resultFuncArgs: SelectorResultArray) => Result > -/** - * A selector that is assumed to have one additional argument, such as - * the props from a React component. - * - * @public - */ -export type ParametricSelector = Selector< - State, - Result, - [Props, ...any] -> - -/** - * A generated selector that is assumed to have one additional argument. - * - * @public - */ -export type OutputParametricSelector = ParametricSelector< - State, - Props, - Result -> & - OutputSelectorFields - /** * A standard function returning true if two values are considered equal. * @@ -311,50 +322,37 @@ export type GetParamsFromSelectors = ArrayTail< MergeParameters > -/* - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - * - * Reselect Internal Utility Types - * - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - */ - -/** - * Any function with any arguments. - * - * @internal - */ -export type AnyFunction = (...args: any[]) => any - -/** - * 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. * * @template FunctionType - The type of the function that is memoized. * - * @internal + * @public */ 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. + * 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 T - Type to be checked. - * @template FallbackTo - Type to fallback to if `T` resolves to `never`. + * @template MemoizeFunction - The type of the memoize function to be checked. * - * @internal + * @public */ -export type FallbackIfNever = IfNever +export type MemoizeOptionsFromParameters< + MemoizeFunction extends UnknownMemoizer +> = + | ( + | NonFunctionType[0]> + | FunctionType[0]> + ) + | ( + | NonFunctionType[number]> + | FunctionType[number]> + )[] /** * Derive the type of memoize options object based on whether the memoize function itself was overridden. @@ -364,66 +362,100 @@ 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 + * @public */ export type OverrideMemoizeOptions< MemoizeFunction extends UnknownMemoizer, OverrideMemoizeFunction extends UnknownMemoizer = never > = IfNever< OverrideMemoizeFunction, - MemoizeOptionsFromParameters, - MemoizeOptionsFromParameters + Simplify>, + Simplify> > /** - * Extracts the non-function part of a type. + * Extracts the additional properties or methods that a memoize function attaches to + * the function it memoizes (e.g., `clearCache`). * - * @template T - The input type to be refined by excluding function types and index signatures. + * @template MemoizeFunction - The type of the memoize function to be checked. + * + * @public + */ +export type ExtractMemoizerFields = + Simplify>> + +/** + * Represents the additional properties attached to a function memoized by `reselect`. + * + * `defaultMemoize`, `weakMapMemoize` and `autotrackMemoize` all return these properties. + * + * @see {@linkcode ExtractMemoizerFields ExtractMemoizerFields} + * + * @public + */ +export type DefaultMemoizeFields = { + /** + * Clears the memoization cache associated with a memoized function. + * This method is typically used to reset the state of the cache, allowing + * for the garbage collection of previously memoized results and ensuring + * that future calls to the function recompute the results. + */ + clearCache: () => void +} + +/* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * + * Reselect Internal Utility Types + * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + */ + +/** + * Any function with any arguments. * * @internal */ -export type NonFunctionType = OmitIndexSignature> +export type AnyFunction = (...args: any[]) => any /** - * Extracts the function part of a type. + * Any function with unknown arguments. * - * @template T - The input type to be refined by extracting function types. + * @internal + */ +export type UnknownFunction = (...args: unknown[]) => unknown + +/** + * 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 FunctionType = Extract +export type FallbackIfNever = IfNever /** - * 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. + * Extracts the non-function part of a type. * - * @template MemoizeFunction - The type of the memoize function to be checked. + * @template T - The input type to be refined by excluding function types and index signatures. * * @internal */ -export type MemoizeOptionsFromParameters< - MemoizeFunction extends UnknownMemoizer -> = - | ( - | Simplify[0]>> - | FunctionType[0]> - ) - | ( - | Simplify[number]>> - | FunctionType[number]> - )[] +export type NonFunctionType = Simplify< + OmitIndexSignature> +> /** - * Extracts the additional fields that a memoize function attaches to - * the function it memoizes (e.g., `clearCache`). + * Extracts the function part of a type. * - * @template MemoizeFunction - The type of the memoize function to be checked. + * @template T - The input type to be refined by extracting function types. * * @internal */ -export type ExtractMemoizerFields = - Simplify>> +export type FunctionType = Extract /** * Extracts the return type from all functions as a tuple. @@ -432,7 +464,7 @@ export type ExtractMemoizerFields = */ export type ExtractReturnType = { [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] - ? ReturnType + ? FallbackIfUnknown, any> : never } @@ -466,11 +498,11 @@ export type Distribute = T extends T ? T : never * * @internal */ -export type FirstArrayElement = TArray extends readonly [ +export type FirstArrayElement = ArrayType extends readonly [ unknown, ...unknown[] ] - ? TArray[0] + ? ArrayType[0] : never /** @@ -478,11 +510,11 @@ export type FirstArrayElement = TArray extends readonly [ * * @internal */ -export type ArrayTail = TArray extends readonly [ +export type ArrayTail = ArrayType extends readonly [ unknown, - ...infer TTail + ...infer Tail ] - ? TTail + ? Tail : [] /** @@ -564,7 +596,8 @@ export type UnionToIntersection = ) extends // Infer the `Intersection` type since TypeScript represents the positional // arguments of unions of functions as an intersection of the union. (mergedIntersection: infer Intersection) => void - ? Intersection + ? // The `& Union` is to allow indexing by the resulting type + Intersection & Union : never /** @@ -612,6 +645,42 @@ export type ObjectValuesToTuple< ? ObjectValuesToTuple : R +/** + * Create a type that makes the given keys required. + * The remaining keys are kept as is. + * + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts Source} + * + * @internal + */ +export type SetRequired = Omit< + BaseType, + Keys +> & + Required> + +/** + * An if-else-like type that resolves depending on whether the given type is `unknown`. + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-unknown.d.ts Source} + * + * @internal + */ +export type IfUnknown = unknown extends T // `T` can be `unknown` or `any` + ? [T] extends [null] // `any` can be `null`, but `unknown` can't be + ? TypeIfNotUnknown + : TypeIfUnknown + : TypeIfNotUnknown + +/** + * When a type is resolves to `unknown`, fallback to a different type. + * + * @template T - Type to be checked. + * @template FallbackTo - Type to fallback to if `T` resolves to `unknown`. + * + * @internal + */ +export type FallbackIfUnknown = IfUnknown + /** * * ----------------------------------------------------------------------------- @@ -717,9 +786,11 @@ export type ExpandFunction = * * @internal */ -export type Simplify = { - [KeyType in keyof T]: T[KeyType] -} & AnyNonNullishValue +export type Simplify = T extends AnyFunction + ? T + : { + [KeyType in keyof T]: T[KeyType] + } & AnyNonNullishValue /** * Fully expand a type, deeply diff --git a/src/utils.ts b/src/utils.ts index ceabd2356..54579edf5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -73,7 +73,7 @@ export function assertIsArrayOfFunctions( * @param item - The item to be checked. * @returns An array containing the input item. If the input is already an array, it's returned without modification. */ -export const ensureIsArray = (item: T | T[]) => { +export const ensureIsArray = (item: unknown) => { return Array.isArray(item) ? item : [item] } diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 5be0dfc43..10092b40a 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -4,65 +4,114 @@ import type { AnyFunction } from '@internal/types' /** + * Represents the longest array within an array of arrays. + * + * @template ArrayOfTuples An array of arrays. + * * @internal */ -type LongestTuple = T extends [ - infer U extends unknown[] -] - ? U - : T extends [infer U, ...infer R extends unknown[][]] - ? MostProperties> - : never +type LongestTuple = + ArrayOfTuples extends [infer FirstArray extends unknown[]] + ? FirstArray + : ArrayOfTuples extends [ + infer FirstArray, + ...infer RestArrays extends unknown[][] + ] + ? LongerOfTwo> + : never /** + * Determines the longer of two array types. + * + * @template ArrayOne First array type. + * @template ArrayTwo Second array type. + * * @internal */ -type MostProperties = keyof U extends keyof T ? T : U +type LongerOfTwo = keyof ArrayTwo extends keyof ArrayOne + ? ArrayOne + : ArrayTwo /** + * Extracts the element at a specific index in an array. + * + * @template ArrayType The array type. + * @template Index The index type. + * * @internal */ -type ElementAt = N extends keyof T - ? T[N] - : unknown +type ElementAt< + ArrayType extends unknown[], + Index extends PropertyKey +> = Index extends keyof ArrayType ? ArrayType[Index] : unknown /** + * Maps each array in an array of arrays to its element at a given index. + * + * @template ArrayOfTuples An array of arrays. + * @template Index The index to extract from each array. + * * @internal */ -type ElementsAt = { - [K in keyof T]: ElementAt +type ElementsAtGivenIndex< + ArrayOfTuples extends readonly unknown[][], + Index extends PropertyKey +> = { + [ArrayIndex in keyof ArrayOfTuples]: ElementAt< + ArrayOfTuples[ArrayIndex], + Index + > } /** + * Computes the intersection of all types in a tuple. + * + * @template Tuple A tuple of types. + * * @internal */ -type Intersect = T extends [] +type Intersect = Tuple extends [] ? unknown - : T extends [infer H, ...infer T] - ? H & Intersect - : T[number] + : Tuple extends [infer Head, ...infer Tail] + ? Head & Intersect + : Tuple[number] /** + * Merges a tuple of arrays into a single tuple, intersecting types at each index. + * + * @template ArrayOfTuples An array of tuples. + * @template LongestArray The longest array in ArrayOfTuples. + * * @internal */ type MergeTuples< - T extends readonly unknown[][], - L extends unknown[] = LongestTuple + ArrayOfTuples extends readonly unknown[][], + LongestArray extends unknown[] = LongestTuple > = { - [K in keyof L]: Intersect> + [Index in keyof LongestArray]: Intersect< + ElementsAtGivenIndex + > } /** + * Extracts the parameter types from a tuple of functions. + * + * @template FunctionsArray An array of function types. + * * @internal */ -type ExtractParameters = { - [K in keyof T]: Parameters +type ExtractParameters = { + [Index in keyof FunctionsArray]: Parameters } /** + * Merges the parameters of a tuple of functions into a single tuple. + * + * @template FunctionsArray An array of function types. + * * @internal */ -export type MergeParameters = - '0' extends keyof T - ? MergeTuples> - : Parameters +export type MergeParameters = + '0' extends keyof FunctionsArray + ? MergeTuples> + : Parameters diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index f92061616..7299d9c72 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -1,22 +1,46 @@ // Original source: // - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js -import type { AnyFunction } from './types' +import type { AnyFunction, DefaultMemoizeFields, Simplify } from './types' const UNTERMINATED = 0 const TERMINATED = 1 interface UnterminatedCacheNode { + /** + * Status, represents whether the cached computation returned a value or threw an error. + */ s: 0 + /** + * Value, either the cached result or an error, depending on status. + */ v: void + /** + * Object cache, a `WeakMap` where non-primitive arguments are stored. + */ o: null | WeakMap> + /** + * Primitive cache, a regular Map where primitive arguments are stored. + */ p: null | Map> } interface TerminatedCacheNode { + /** + * Status, represents whether the cached computation returned a value or threw an error. + */ s: 1 + /** + * Value, either the cached result or an error, depending on status. + */ v: T + /** + * Object cache, a `WeakMap` where non-primitive arguments are stored. + */ o: null | WeakMap> + /** + * Primitive cache, a regular `Map` where primitive arguments are stored. + */ p: null | Map> } @@ -24,21 +48,21 @@ type CacheNode = TerminatedCacheNode | UnterminatedCacheNode function createCacheNode(): CacheNode { return { - s: UNTERMINATED, // status, represents whether the cached computation returned a value or threw an error - v: undefined, // value, either the cached result or an error, depending on s - o: null, // object cache, a WeakMap where non-primitive arguments are stored - p: null // primitive cache, a regular Map where primitive arguments are stored. + s: UNTERMINATED, + v: undefined, + o: null, + p: null } } /** * 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. + * 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`:__ + * __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. @@ -47,7 +71,7 @@ function createCacheNode(): CacheNode { * They're based on strict reference equality. * - It's roughly the same speed as `defaultMemoize`, although likely a fraction slower. * - * __Use Cases for `weakmapMemoize`:__ + * __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 @@ -63,13 +87,20 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * interface RootState { + * items: { id: number; category: string; name: string }[] + * } + * + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos) => todos[id], - * { memoize: weakMapMemoize } + * (items, category) => items.filter(item => item.category === category), + * { + * memoize: weakMapMemoize, + * argsMemoize: weakMapMemoize + * } * ) * ``` * @@ -78,14 +109,14 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelectorCreator, weakMapMemoize } from 'reselect' * - * const createSelectorWeakmap = createSelectorCreator(weakMapMemoize) + * const createSelectorWeakMap = createSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) * - * const selectTodoById = createSelectorWeakmap( + * const selectItemsByCategory = createSelectorWeakMap( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos) => todos[id] + * (items, category) => items.filter(item => item.category === category) * ) * ``` * @@ -98,14 +129,12 @@ function createCacheNode(): CacheNode { * @experimental */ export function weakMapMemoize(func: Func) { - // we reference arguments instead of spreading them for performance reasons - let fnNode = createCacheNode() function memoized() { let cacheNode = fnNode - - for (let i = 0, l = arguments.length; i < l; i++) { + const { length } = arguments + for (let i = 0, l = length; i < l; i++) { const arg = arguments[i] if ( typeof arg === 'function' || @@ -153,5 +182,5 @@ export function weakMapMemoize(func: Func) { fnNode = createCacheNode() } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 12d4a14ce..726f18d16 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,7 +1,10 @@ -import { createSelectorCreator, unstable_autotrackMemoize as 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 +const numOfStates = 1_000_000 interface StateA { a: number } diff --git a/test/createStructuredSelector.spec.ts b/test/createStructuredSelector.spec.ts index 058c9aba5..56cc78013 100644 --- a/test/createStructuredSelector.spec.ts +++ b/test/createStructuredSelector.spec.ts @@ -128,4 +128,45 @@ describe('structured selector created with createStructuredSel ) } ) + + localTest( + 'structured selector invalid args can throw runtime errors', + ({ state }) => { + const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: ( + state: RootState, + id: number, + field: keyof RootState['todos'][number] + ) => state.todos[id][field] + }, + createSelector + ) + const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + ( + state: RootState, + id: number, + field: keyof RootState['todos'][number] + ) => state.todos[id][field] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } + ) + // These two cases are the same. + // @ts-expect-error + expect(() => structuredSelector(state)).toThrowError(TypeError) + // @ts-expect-error + expect(() => selector(state)).toThrowError(TypeError) + } + ) }) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index dfb4abfba..99d236835 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -11,11 +11,11 @@ import { } from 'reselect' import type { OutputSelector, OutputSelectorFields } from 'reselect' -import type { LocalTestContext, RootState } from './testUtils' -import { addTodo, deepClone, setupStore, toggleCompleted } from './testUtils' +import type { RootState } from './testUtils' +import { addTodo, deepClone, localTest, 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 +const numOfStates = 1_000_000 interface StateA { a: number } @@ -395,49 +395,37 @@ describe('Customizing selectors', () => { expect(memoizer3Calls).toBeGreaterThan(0) }) - test.todo('Test order of execution in a selector', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - } - // original options untouched. - const selectorOriginal = createSelector( - (state: State) => state.todos, - todos => todos.map(({ id }) => id), - { - inputStabilityCheck: 'always', - memoizeOptions: { - equalityCheck: (a, b) => false, - resultEqualityCheck: (a, b) => false + localTest.todo( + 'Test order of execution in a selector', + ({ store, state }) => { + // original options untouched. + const selectorOriginal = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + inputStabilityCheck: 'always', + memoizeOptions: { + equalityCheck: (a, b) => false, + resultEqualityCheck: (a, b) => false + } } - } - ) - selectorOriginal(deepClone(state)) - selectorOriginal(deepClone(state)) - const selectorDefaultParametric = createSelector( - [(state: State, id: number) => id, (state: State) => state.todos], - (id, todos) => todos.filter(todo => todo.id === id) - ) - selectorDefaultParametric(state, 1) - selectorDefaultParametric(state, 1) - }) + ) + selectorOriginal(deepClone(state)) + selectorOriginal(deepClone(state)) + const selectorDefaultParametric = createSelector( + [ + (state: RootState, id: number) => id, + (state: RootState) => state.todos + ], + (id, todos) => todos.filter(todo => todo.id === id) + ) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(state, 1) + } + ) }) -describe('argsMemoize and memoize', localTest => { - beforeEach(context => { - const store = setupStore() - context.store = store - context.state = store.getState() - }) - +describe('argsMemoize and memoize', () => { localTest('passing memoize directly to createSelector', ({ store }) => { const state = store.getState() const selectorDefault = createSelector( @@ -470,7 +458,9 @@ describe('argsMemoize and memoize', localTest => { 'lastResult', 'dependencies', 'recomputations', - 'resetRecomputations' + 'resetRecomputations', + 'dependencyRecomputations', + 'resetDependencyRecomputations' ] const memoizerFields: Exclude< keyof OutputSelector, @@ -491,7 +481,9 @@ describe('argsMemoize and memoize', localTest => { 'lastResult' in selector && 'dependencies' in selector && 'recomputations' in selector && + 'dependencyRecomputations' in selector && 'resetRecomputations' in selector && + 'resetDependencyRecomputations' in selector && 'memoize' in selector && 'argsMemoize' in selector && typeof selector.resultFunc === 'function' && @@ -499,7 +491,9 @@ describe('argsMemoize and memoize', localTest => { typeof selector.lastResult === 'function' && Array.isArray(selector.dependencies) && typeof selector.recomputations === 'function' && + typeof selector.dependencyRecomputations === 'function' && typeof selector.resetRecomputations === 'function' && + typeof selector.resetDependencyRecomputations === 'function' && typeof selector.memoize === 'function' && typeof selector.argsMemoize === 'function' && selector.dependencies.length >= 1 && @@ -529,7 +523,9 @@ describe('argsMemoize and memoize', localTest => { expect(selectorDefault.lastResult).to.be.a('function') expect(selectorDefault.dependencies).to.be.an('array').that.is.not.empty expect(selectorDefault.recomputations).to.be.a('function') + expect(selectorDefault.dependencyRecomputations).to.be.a('function') expect(selectorDefault.resetRecomputations).to.be.a('function') + expect(selectorDefault.resetDependencyRecomputations).to.be.a('function') expect(selectorDefault.memoize).to.be.a('function') expect(selectorDefault.argsMemoize).to.be.a('function') expect(selectorDefault.clearCache).to.be.a('function') @@ -539,7 +535,9 @@ describe('argsMemoize and memoize', localTest => { expect(selectorAutotrack.recomputations()).toBe(0) expect(selectorDefault(state)).toStrictEqual(selectorAutotrack(state)) expect(selectorDefault.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(1) // flipping completed flag does not cause the autotrack memoizer to re-run. store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) @@ -552,14 +550,18 @@ describe('argsMemoize and memoize', localTest => { const defaultSelectorLastResult2 = selectorDefault.lastResult() const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(3) + expect(selectorDefault.dependencyRecomputations()).toBe(3) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(3) for (let i = 0; i < 10; i++) { store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) selectorAutotrack(store.getState()) } expect(selectorDefault.recomputations()).toBe(13) + expect(selectorDefault.dependencyRecomputations()).toBe(13) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(13) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) // Default memoize does not preserve referential equality but autotrack does. expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -571,6 +573,7 @@ describe('argsMemoize and memoize', localTest => { ) selectorAutotrack(store.getState()) expect(selectorAutotrack.recomputations()).toBe(2) + expect(selectorAutotrack.dependencyRecomputations()).toBe(14) }) localTest('passing argsMemoize directly to createSelector', ({ store }) => { @@ -593,6 +596,8 @@ describe('argsMemoize and memoize', localTest => { ) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(1) selectorDefault(store.getState()) selectorAutotrack(store.getState()) // toggling the completed flag should force the default memoizer to recalculate but not autotrack. @@ -612,6 +617,8 @@ describe('argsMemoize and memoize', localTest => { store.dispatch(toggleCompleted(2)) expect(selectorDefault.recomputations()).toBe(4) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(4) + expect(selectorAutotrack.dependencyRecomputations()).toBe(4) selectorDefault(store.getState()) selectorAutotrack(store.getState()) store.dispatch(toggleCompleted(0)) @@ -627,6 +634,8 @@ describe('argsMemoize and memoize', localTest => { const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(6) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(6) + expect(selectorAutotrack.dependencyRecomputations()).toBe(7) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -640,6 +649,8 @@ describe('argsMemoize and memoize', localTest => { } expect(selectorAutotrack.recomputations()).toBe(1) expect(selectorDefault.recomputations()).toBe(16) + expect(selectorAutotrack.dependencyRecomputations()).toBe(17) + expect(selectorDefault.dependencyRecomputations()).toBe(16) // original options untouched. const selectorOriginal = createSelector( [(state: RootState) => state.todos], @@ -672,6 +683,8 @@ describe('argsMemoize and memoize', localTest => { } expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) expect(selectorOriginal.recomputations()).toBe(11) + expect(selectorOverrideArgsMemoize.dependencyRecomputations()).toBe(1) + expect(selectorOriginal.dependencyRecomputations()).toBe(11) const selectorDefaultParametric = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id) @@ -679,11 +692,14 @@ describe('argsMemoize and memoize', localTest => { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(1) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(1) selectorDefaultParametric(store.getState(), 2) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(3) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(3) selectorDefaultParametric(store.getState(), 2) expect(selectorDefaultParametric.recomputations()).toBe(4) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(4) const selectorDefaultParametricArgsWeakMap = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id), @@ -697,14 +713,23 @@ describe('argsMemoize and memoize', localTest => { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(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) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(2) for (let i = 0; i < 10; i++) { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) @@ -713,6 +738,9 @@ describe('argsMemoize and memoize', localTest => { selectorDefaultParametricArgsWeakMap(store.getState(), 5) } expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(5) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(5) for (let i = 0; i < 10; i++) { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 2) @@ -721,6 +749,7 @@ describe('argsMemoize and memoize', localTest => { selectorDefaultParametric(store.getState(), 5) } expect(selectorDefaultParametric.recomputations()).toBe(54) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(54) for (let i = 0; i < 10; i++) { selectorDefaultParametricWeakMap(store.getState(), 1) selectorDefaultParametricWeakMap(store.getState(), 2) @@ -729,6 +758,7 @@ describe('argsMemoize and memoize', localTest => { selectorDefaultParametricWeakMap(store.getState(), 5) } expect(selectorDefaultParametricWeakMap.recomputations()).toBe(5) + expect(selectorDefaultParametricWeakMap.dependencyRecomputations()).toBe(50) }) localTest('passing argsMemoize to createSelectorCreator', ({ store }) => { @@ -775,7 +805,7 @@ describe('argsMemoize and memoize', localTest => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoize.recomputations()).to.be.a('number') - expect(selectorMicroMemoize.resetRecomputations()).toBe(0) + expect(selectorMicroMemoize.dependencyRecomputations()).to.be.a('number') expect(selectorMicroMemoize.resultFunc).to.be.a('function') expect( selectorMicroMemoize.resultFunc([ @@ -839,7 +869,13 @@ describe('argsMemoize and memoize', localTest => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoizeOverridden.recomputations()).to.be.a('number') + expect(selectorMicroMemoizeOverridden.dependencyRecomputations()).to.be.a( + 'number' + ) expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBe(0) + expect(selectorMicroMemoizeOverridden.resetDependencyRecomputations()).toBe( + 0 + ) expect( selectorMicroMemoizeOverridden.resultFunc([ { @@ -914,9 +950,15 @@ describe('argsMemoize and memoize', localTest => { expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() ).to.be.a('number') + expect( + selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencyRecomputations() + ).to.be.a('number') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() ).toBe(0) + expect( + selectorMicroMemoizeOverrideArgsMemoizeOnly.resetDependencyRecomputations() + ).toBe(0) expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ { @@ -951,7 +993,9 @@ describe('argsMemoize and memoize', localTest => { 'recomputations', 'resetRecomputations', 'memoize', - 'argsMemoize' + 'argsMemoize', + 'dependencyRecomputations', + 'resetDependencyRecomputations' ]) expect(selectorMicroMemoizeOverrideMemoizeOnly.cache).to.be.an('object') expect(selectorMicroMemoizeOverrideMemoizeOnly.fn).to.be.a('function') @@ -984,9 +1028,15 @@ describe('argsMemoize and memoize', localTest => { expect(selectorMicroMemoizeOverrideMemoizeOnly.recomputations()).to.be.a( 'number' ) + expect( + selectorMicroMemoizeOverrideMemoizeOnly.dependencyRecomputations() + ).to.be.a('number') expect(selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations()).toBe( 0 ) + expect( + selectorMicroMemoizeOverrideMemoizeOnly.resetDependencyRecomputations() + ).toBe(0) expect( selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ { @@ -999,22 +1049,92 @@ describe('argsMemoize and memoize', localTest => { ).to.be.an('array').that.is.not.empty }) - localTest('pass options object to createSelectorCreator ', ({ store }) => { - const createSelectorMicro = createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b } - }) - const selectorMicro = createSelectorMicro( - [(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]` + localTest( + 'pass options object to createSelectorCreator ', + ({ store, state }) => { + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b } + }) + const selectorMicro = createSelectorMicro( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) ) - ) - }) + const selector = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selector1 = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + expect(() => + //@ts-expect-error + createSelectorMicro([(state: RootState) => state.todos], 'a') + ).toThrowError( + TypeError( + `createSelector expects an output function after the inputs, but received: [string]` + ) + ) + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + let called = 0 + const selectorDefault2 = createSelector( + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => { + called++ + return state.users + }, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState, id: number) => state.users, + users => { + return users.user.details.preferences.notifications.push.frequency + }, + { inputStabilityCheck: 'never' } + ) + const start = performance.now() + for (let i = 0; i < 10_000_000; i++) { + selectorDefault(state) + } + expect(performance.now() - start).toBeLessThan(1000) + selectorDefault1(state) + const stateBeforeChange = store.getState() + selectorDefault2(store.getState(), 0) + const stateBeforeChange1 = store.getState() + store.dispatch(toggleCompleted(0)) + const stateAfterChange = store.getState() + expect(stateBeforeChange1).not.toBe(stateAfterChange) + expect(stateBeforeChange1.alerts).toBe(stateAfterChange.alerts) + expect(stateBeforeChange1.todos[1]).toBe(stateAfterChange.todos[1]) + expect(stateBeforeChange1).toBe(stateBeforeChange) + expect(stateBeforeChange1.alerts).toBe(stateBeforeChange.alerts) + } + ) }) diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 2e34b4d87..78daee563 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect' +import type { StateA, StateAB } from 'testTypes' describe('createSelector exposed utils', () => { test('resetRecomputations', () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index f0d096ea8..faeb41447 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,5 +1,13 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import { test } from 'vitest' +import type { + AnyFunction, + OutputSelector, + Selector, + SelectorArray, + Simplify +} from '../src/types' interface Todo { id: number @@ -15,6 +23,63 @@ interface Alert { read: boolean } +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays const todoState = [ { id: 0, @@ -60,6 +125,25 @@ const todoState = [ } ] +export const createTodoItem = (id: number) => { + return { + id, + title: `Task ${id}`, + description: `Description for task ${id}`, + completed: false + } +} + +export const pushToTodos = (howMany: number) => { + const { length: todoStateLength } = todoState + const limit = howMany + todoStateLength + for (let i = todoStateLength; i < limit; i++) { + todoState.push(createTodoItem(i)) + } +} + +pushToTodos(200) + const alertState = [ { id: 0, @@ -103,6 +187,49 @@ const alertState = [ } ] +// For nested fields tests +const userState: UserState = { + user: { + id: 0, + details: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345', + billing: { + street: '456 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345' + } + }, + preferences: { + newsletter: true, + notifications: { + email: true, + sms: false, + push: { + enabled: true, + frequency: 'daily' + } + } + } + }, + status: 'active', + login: { + lastLogin: '2023-04-30T12:34:56Z', + loginCount: 123 + } + }, + appSettings: { + theme: 'dark', + language: 'en-US' + } +} + const todoSlice = createSlice({ name: 'todos', initialState: todoState, @@ -151,6 +278,13 @@ const alertSlice = createSlice({ } }, + toggleRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = !alert.read + } + }, + addAlert: (state, action: PayloadAction>) => { const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 state.push({ @@ -165,14 +299,79 @@ const alertSlice = createSlice({ } }) +const userSlice = createSlice({ + name: 'users', + initialState: userState, + reducers: { + setUserName: (state, action: PayloadAction) => { + state.user.details.name = action.payload + }, + + setUserEmail: (state, action: PayloadAction) => { + state.user.details.email = action.payload + }, + + setAppTheme: (state, action: PayloadAction) => { + state.appSettings.theme = action.payload + }, + + updateUserStatus: (state, action: PayloadAction) => { + state.user.status = action.payload + }, + + updateLoginDetails: ( + state, + action: PayloadAction<{ lastLogin: string; loginCount: number }> + ) => { + state.user.login = { ...state.user.login, ...action.payload } + }, + + updateUserAddress: (state, action: PayloadAction
) => { + state.user.details.address = { + ...state.user.details.address, + ...action.payload + } + }, + + updateBillingAddress: (state, action: PayloadAction) => { + state.user.details.address.billing = { + ...state.user.details.address.billing, + ...action.payload + } + }, + + toggleNewsletterSubscription: state => { + state.user.details.preferences.newsletter = + !state.user.details.preferences.newsletter + }, + + setNotificationPreferences: ( + state, + action: PayloadAction + ) => { + state.user.details.preferences.notifications = { + ...state.user.details.preferences.notifications, + ...action.payload + } + }, + + updateAppLanguage: (state, action: PayloadAction) => { + state.appSettings.language = action.payload + } + } +}) + const rootReducer = combineReducers({ [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer + [alertSlice.name]: alertSlice.reducer, + [userSlice.name]: userSlice.reducer }) -export const setupStore = () => configureStore({ reducer: rootReducer }) +export const setupStore = (preloadedState?: Partial) => { + return configureStore({ reducer: rootReducer, preloadedState }) +} -export type AppStore = ReturnType +export type AppStore = Simplify> export type RootState = ReturnType @@ -181,7 +380,8 @@ export interface LocalTestContext { state: RootState } -export const { markAsRead, addAlert, removeAlert } = alertSlice.actions +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions export const { toggleCompleted, @@ -191,6 +391,50 @@ export const { clearCompleted } = todoSlice.actions +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions + // Since Node 16 does not support `structuredClone` export const deepClone = (object: T): T => JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = ( + selector: S +) => { + selector.clearCache() + selector.resetRecomputations() + selector.resetDependencyRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = < + S extends OutputSelector +>( + selector: S +) => { + console.log( + `${selector.name} result function recalculated:`, + selector.recomputations(), + `time(s)`, + `input selectors recalculated:`, + selector.dependencyRecomputations(), + `time(s)` + ) +} diff --git a/test/tsconfig.json b/test/tsconfig.json index dc51ba69e..27e1b8705 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,12 +3,12 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "Node", "emitDeclarationOnly": false, "strict": true, "noEmit": true, - "target": "esnext", + "target": "ESNext", "jsx": "react", "baseUrl": ".", "rootDir": ".", @@ -18,8 +18,10 @@ "types": ["vitest/globals"], "paths": { "reselect": ["../src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["src/*"] + "@internal/*": ["../src/*"] } }, - "include": ["**/*.ts"] + "include": [ + "**/*.ts", + ] } diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 369e121ad..999d36248 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,7 +1,7 @@ import { createSelectorCreator, weakMapMemoize } 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 +const numOfStates = 1_000_000 interface StateA { a: number } @@ -23,7 +23,7 @@ for (let i = 0; i < numOfStates; i++) { states.push({ a: 1, b: 2 }) } -describe('Basic selector behavior with autotrack', () => { +describe('Basic selector behavior with weakMapMemoize', () => { const createSelector = createSelectorCreator(weakMapMemoize) test('basic selector', () => { diff --git a/tsconfig.json b/tsconfig.json index d36a9f630..388fab465 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "skipLibCheck": true, "allowJs": true, "jsx": "react", + "noErrorTruncation": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "./es", @@ -19,7 +20,7 @@ "baseUrl": ".", "paths": { "reselect": ["src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["src/*"], + "@internal/*": ["src/*"] } }, "include": ["./src/**/*"], diff --git a/type-tests/argsMemoize.test-d.ts b/type-tests/argsMemoize.test-d.ts new file mode 100644 index 000000000..1d58e8ee7 --- /dev/null +++ b/type-tests/argsMemoize.test-d.ts @@ -0,0 +1,894 @@ +import memoizeOne from 'memoize-one' +import microMemoize from 'micro-memoize' +import { + createSelector, + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { assertType, describe, expectTypeOf, test } from 'vitest' + +interface RootState { + todos: { + id: number + completed: boolean + }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +describe('memoize and argsMemoize', () => { + test('Override Only Memoize In createSelector', () => { + const selectorDefaultSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const selectorDefaultArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorAutotrackArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: 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: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = + // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. + createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorWeakMapSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorWeakMapArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const changeMemoizeMethodSelectorDefault = createSelectorDefault( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = + // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. + createSelectorDefault( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = + createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. + ) + const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = + createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. + ) + }) + + test('Override Only argsMemoize In createSelector', () => { + const selectorDefaultSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const selectorDefaultArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const selectorAutotrackArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: 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: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: autotrackMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = + // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. + createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: autotrackMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + const selectorWeakMapSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const selectorWeakMapArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( + [ + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id) + ], + { + argsMemoize: weakMapMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) + const createSelectorDefaultMemoize = createSelectorCreator({ + memoize: defaultMemoize + }) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + // memoizeOptions: [], + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ maxSize: 2 }] + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = + // @ts-expect-error + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: + // @ts-expect-error + (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = + // @ts-expect-error + createSelectorDefaultMemoize( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoize: weakMapMemoize, + memoizeOptions: [], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const changeMemoizeMethodSelectorDefault = createSelectorDefault( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = + // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. + createSelectorDefault( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = + createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. + ) + const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = + createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. + ) + }) + + test('Override memoize And argsMemoize In createSelector', () => { + const createSelectorMicroMemoize = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: [{ isEqual: (a, b) => a === b }], + // memoizeOptions: { isEqual: (a, b) => a === b }, + argsMemoize: microMemoize, + argsMemoizeOptions: { isEqual: (a, b) => a === b } + }) + const selectorMicroMemoize = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + assertType(selectorMicroMemoize(state)) + // @ts-expect-error + selectorMicroMemoize() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoize.cache + selectorMicroMemoize.fn() + selectorMicroMemoize.isMemoized + selectorMicroMemoize.options + // @ts-expect-error + selectorMicroMemoize.clearCache() + // Checking existence of fields related to `memoize` + selectorMicroMemoize.memoizedResultFunc.cache + selectorMicroMemoize.memoizedResultFunc.fn() + selectorMicroMemoize.memoizedResultFunc.isMemoized + selectorMicroMemoize.memoizedResultFunc.options + // @ts-expect-error + selectorMicroMemoize.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + selectorMicroMemoize.dependencies + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoize.dependencies) + assertType(selectorMicroMemoize.lastResult()) + // @ts-expect-error + selectorMicroMemoize.memoizedResultFunc() + assertType( + selectorMicroMemoize.memoizedResultFunc([{ id: 0, completed: true }]) + ) + selectorMicroMemoize.recomputations() + selectorMicroMemoize.resetRecomputations() + // @ts-expect-error + selectorMicroMemoize.resultFunc() + assertType( + selectorMicroMemoize.resultFunc([{ id: 0, completed: true }]) + ) + + // Checking to see if types dynamically change if memoize or argsMemoize are overridden inside `createSelector`. + // `microMemoize` was initially passed into `createSelectorCreator` + // as `memoize` and `argsMemoize`, After overriding them both to `defaultMemoize`, + // not only does the type for `memoizeOptions` and `argsMemoizeOptions` change to + // the options parameter of `defaultMemoize`, the output selector fields + // also change their type to the return type of `defaultMemoize`. + const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, + argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } + } + ) + assertType(selectorMicroMemoizeOverridden(state)) + // @ts-expect-error + selectorMicroMemoizeOverridden() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverridden.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeOverridden.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.options + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverridden.dependencies) + assertType( + selectorMicroMemoizeOverridden.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverridden.memoizedResultFunc() + selectorMicroMemoizeOverridden.recomputations() + selectorMicroMemoizeOverridden.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverridden.resultFunc() + assertType( + selectorMicroMemoizeOverridden.resultFunc([{ id: 0, completed: true }]) + ) + // Making sure the type behavior is consistent when args are passed in as an array. + const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( + [(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 } + } + ) + assertType(selectorMicroMemoizeOverriddenArray(state)) + // @ts-expect-error + selectorMicroMemoizeOverriddenArray() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverriddenArray.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.options + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverriddenArray.dependencies) + assertType( + selectorMicroMemoizeOverriddenArray.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverriddenArray.memoizedResultFunc() + selectorMicroMemoizeOverriddenArray.recomputations() + selectorMicroMemoizeOverriddenArray.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverriddenArray.resultFunc() + assertType( + selectorMicroMemoizeOverriddenArray.resultFunc([ + { id: 0, completed: true } + ]) + ) + const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = + // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + argsMemoize: defaultMemoize, + memoizeOptions: { + isPromise: false, + resultEqualityCheck: + // @ts-expect-error + (a, b) => a === b + }, + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + const selectorMicroMemoizeOverrideArgsMemoizeOnly = + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + argsMemoize: defaultMemoize, + memoizeOptions: { isPromise: false }, + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + assertType(selectorMicroMemoizeOverrideArgsMemoizeOnly(state)) + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverrideArgsMemoizeOnly.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.options + + // Checking existence of fields related to `memoize`, these should still be the same. + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.cache + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.fn() + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.isMemoized + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.options + // @ts-expect-error Note that since we did not override `memoize` in the options object, + // `memoizedResultFunc.clearCache` is still an invalid field access. + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencies) + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.lastResult() + ) + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc() + selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() + selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc() + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ + { id: 0, completed: true } + ]) + ) + + const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + memoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + assertType(selectorMicroMemoizeOverrideMemoizeOnly(state)) + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly() + + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverrideMemoizeOnly.cache + selectorMicroMemoizeOverrideMemoizeOnly.fn + selectorMicroMemoizeOverrideMemoizeOnly.isMemoized + selectorMicroMemoizeOverrideMemoizeOnly.options + // @ts-expect-error Note that since we did not override `argsMemoize` in the options object, + // `selector.clearCache` is still an invalid field access. + selectorMicroMemoizeOverrideMemoizeOnly.clearCache() + + // Checking existence of fields related to `memoize` + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.options + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverrideMemoizeOnly.dependencies) + assertType(selectorMicroMemoizeOverrideMemoizeOnly.lastResult()) + assertType( + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc() + selectorMicroMemoizeOverrideMemoizeOnly.recomputations() + selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly.resultFunc() + assertType( + selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ + { id: 0, completed: true } + ]) + ) + + const selectorMicroMemoizePartiallyOverridden = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden1 = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden2 = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + // memoizeOptions: [ + // { + // equalityCheck: + // // @ts-expect-error + // (a, b) => a === b, + // maxSize: 2 + // } + // ], + argsMemoizeOptions: [{ isPromise: false }] + } + ) + + const selectorDefaultParametric = createSelector( + (state: RootState, id: number) => id, + (state: RootState) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id), + { + argsMemoize: microMemoize, + inputStabilityCheck: 'never', + memoize: memoizeOne, + argsMemoizeOptions: [], + memoizeOptions: [(a, b) => a === b] + } + ) + assertType< + { + id: number + completed: boolean + }[] + >(selectorDefaultParametric(state, 0)) + assertType< + { + id: number + completed: boolean + }[] + >(selectorDefaultParametric(state, 1)) + // @ts-expect-error + selectorDefaultParametric(state) + // @ts-expect-error + selectorDefaultParametric(1) + // @ts-expect-error + selectorDefaultParametric(state, '') + // @ts-expect-error + selectorDefaultParametric(state, 1, 1) + // Checking existence of fields related to `argsMemoize` + // Prior to override, this field did NOT exist. + selectorDefaultParametric.cache + // Prior to override, this field did NOT exist. + selectorDefaultParametric.fn + // Prior to override, this field did NOT exist. + selectorDefaultParametric.isMemoized + // Prior to override, this field did NOT exist. + selectorDefaultParametric.options + // @ts-expect-error Prior to override, this field DID exist. + selectorDefaultParametric.clearCache() + + // Checking existence of fields related to `memoize` + // @ts-expect-error Prior to override, this field DID exist. + selectorDefaultParametric.memoizedResultFunc.clearCache() + // Prior to override, this field did NOT exist. + selectorDefaultParametric.memoizedResultFunc.clear() + + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState, id: number) => number, + (state: RootState) => { id: number; completed: boolean }[] + ] + >(selectorDefaultParametric.dependencies) + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.lastResult() + ) + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.memoizedResultFunc(0, [ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorDefaultParametric.memoizedResultFunc() + selectorDefaultParametric.recomputations() + selectorDefaultParametric.resetRecomputations() + // @ts-expect-error + selectorDefaultParametric.resultFunc() + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.resultFunc(0, [{ id: 0, completed: true }]) + ) + }) + + test('memoize And argsMemoize In createSelectorCreator', () => { + // If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` + // falls back to the options parameter of `defaultMemoize`. + const createSelectorArgsMemoizeOptionsFallbackToDefault = + createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + }) + const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = + createSelectorArgsMemoizeOptionsFallbackToDefault( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault(state) + ) + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.clearCache() + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.cache + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.fn + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.isMemoized + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .cache + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.fn() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .isMemoized + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .options + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.dependencies) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.lastResult() + ) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc( + [{ id: 0, completed: true }] + ) + ) + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.recomputations() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc() + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc([ + { id: 0, completed: true } + ]) + ) + expectTypeOf( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoize + ).toEqualTypeOf(microMemoize) + expectTypeOf( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.argsMemoize + ).toEqualTypeOf(defaultMemoize) + + const createSelectorWithWrongArgsMemoizeOptions = + // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `defaultMemoize`. + createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b }, + argsMemoizeOptions: { + isEqual: + // @ts-expect-error implicit any + (a, b) => a === b + } + }) + + // When passing in an options object as the first argument, there should be no other arguments. + const createSelectorWrong = createSelectorCreator( + { + // @ts-expect-error + memoize: microMemoize, + // @ts-expect-error + memoizeOptions: { isEqual: (a, b) => a === b }, + // @ts-expect-error + argsMemoizeOptions: { equalityCheck: (a, b) => a === b } + }, + [] // This causes the error. + ) + }) + + + test('autotrackMemoize types', () => { + const selector = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + selector.memoizedResultFunc.clearCache + }) +}) diff --git a/type-tests/createSelectorCreator.test-d.ts b/type-tests/createSelectorCreator.test-d.ts new file mode 100644 index 000000000..da5897277 --- /dev/null +++ b/type-tests/createSelectorCreator.test-d.ts @@ -0,0 +1,58 @@ +import lodashMemoize from 'lodash/memoize' +import memoizeOne from 'memoize-one' +import microMemoize from 'micro-memoize' +import { + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +describe('createSelectorCreator', () => { + test('options object as argument', () => { + const createSelectorDefault = createSelectorCreator({ + memoize: defaultMemoize + }) + const createSelectorWeakMap = createSelectorCreator({ + memoize: weakMapMemoize + }) + const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize + }) + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize + }) + const createSelectorOne = createSelectorCreator({ + memoize: memoizeOne + }) + const createSelectorLodash = createSelectorCreator({ + memoize: lodashMemoize + }) + }) + + test('memoize function as argument', () => { + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const createSelectorMicro = createSelectorCreator(microMemoize) + const createSelectorOne = createSelectorCreator(memoizeOne) + const createSelectorLodash = createSelectorCreator(lodashMemoize) + }) +}) diff --git a/type-tests/createStructuredSelector.test-d.ts b/type-tests/createStructuredSelector.test-d.ts new file mode 100644 index 000000000..13f16fbfe --- /dev/null +++ b/type-tests/createStructuredSelector.test-d.ts @@ -0,0 +1,43 @@ +import type { TypedStructuredSelectorCreator } from 'reselect' +import { createStructuredSelector } from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +describe('createStructuredSelector', () => { + test('TypedStructuredSelectorCreator', () => { + const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = + createStructuredSelector + const structuredSelector = typedStructuredSelectorCreator({ + todos: state => state.todos, + alerts: state => state.alerts + }) + structuredSelector(state).alerts + structuredSelector(state).todos + }) + test('parametric', () => { + const structuredSelector = createStructuredSelector({ + todos: (state: RootState) => state.todos, + alerts: (state: RootState) => state.alerts, + todoById: (state: RootState, id: number) => state.todos[id] + }) + structuredSelector(state, 0).alerts + structuredSelector(state, 0).todoById.id + structuredSelector(state, 0).todos + const { alerts, todos, todoById } = structuredSelector(state, 0) + }) +}) diff --git a/type-tests/deepNesting.test-d.ts b/type-tests/deepNesting.test-d.ts new file mode 100644 index 000000000..5a5e69bc2 --- /dev/null +++ b/type-tests/deepNesting.test-d.ts @@ -0,0 +1,320 @@ +import microMemoize from 'micro-memoize' +import { createSelector, defaultMemoize } from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { + id: number + completed: boolean + }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +describe('deep nesting', () => { + test('Deep Nesting First And Second createSelector Overload', () => { + 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 + 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) + selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache + }) + test('Deep Nesting Second createSelector Overload', () => { + 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 + }) + }) + + test('Deep Nesting Third createSelector Overload', () => { + 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) + }) + + test('createSelector Parameter Limit', () => { + const selector = createSelector( + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + ( + foo1: string, + foo2: number, + foo3: boolean, + foo4: string, + foo5: string, + foo6: string, + foo7: string, + foo8: number, + foo9: string[], + foo10: string, + foo11: number, + foo12: boolean, + foo13: string, + foo14: string, + foo15: string, + foo16: string, + foo17: number, + foo18: string[], + foo19: string, + foo20: number, + foo21: boolean, + foo22: string, + foo23: string, + foo24: string, + foo25: string, + foo26: number, + foo27: string[], + foo28: string, + foo29: number, + foo30: boolean, + foo31: string, + foo32: string, + foo33: string, + foo34: string, + foo35: number, + foo36: string[] + ) => { + return { + foo1, + foo2, + foo3, + foo4, + foo5, + foo6, + foo7, + foo8, + foo9, + foo10, + foo11, + foo12, + foo13, + foo14, + foo15, + foo16, + foo17, + foo18, + foo19, + foo20, + foo21, + foo22, + foo23, + foo24, + foo25, + foo26, + foo27, + foo28, + foo29, + foo30, + foo31, + foo32, + foo33, + foo34, + foo35, + foo36 + } + } + ) + }) +}) diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json new file mode 100644 index 000000000..cf6ec4117 --- /dev/null +++ b/type-tests/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "strict": true, + "target": "ES2015", + "declaration": true, + "noEmit": true, + "skipLibCheck": true, + "paths": { + "reselect": ["../src/index"], // @remap-prod-remove-line + "@internal/*": ["../src/*"] + } + } +} diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index acb9b1df0..2093b8920 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -7,15 +7,15 @@ import { unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' -import { expectExactType } from './test' +import { expectExactType } from './typesTestUtils' -interface State { +interface RootState { todos: { id: number completed: boolean }[] } -const state: State = { +const state: RootState = { todos: [ { id: 0, completed: false }, { id: 1, completed: false } @@ -24,69 +24,69 @@ const state: State = { function overrideOnlyMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: 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], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) // @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, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } @@ -95,37 +95,37 @@ function overrideOnlyMemoizeInCreateSelector() { const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @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, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -133,38 +133,38 @@ function overrideOnlyMemoizeInCreateSelector() { function overrideOnlyArgsMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: 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], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { @@ -174,7 +174,7 @@ function overrideOnlyArgsMemoizeInCreateSelector() { ) // @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, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { @@ -183,64 +183,156 @@ function overrideOnlyArgsMemoizeInCreateSelector() { } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( + [ + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id) + ], + { + argsMemoize: weakMapMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) + const createSelectorDefaultMemoize = createSelectorCreator({ + memoize: defaultMemoize + }) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + // memoizeOptions: [], + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ maxSize: 2 }] + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = + // @ts-expect-error + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: + // @ts-expect-error + (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = + // @ts-expect-error + createSelectorDefaultMemoize( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoize: weakMapMemoize, + memoizeOptions: [], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) const createSelectorDefault = createSelectorCreator(defaultMemoize) const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @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, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -249,12 +341,13 @@ function overrideOnlyArgsMemoizeInCreateSelector() { function overrideMemoizeAndArgsMemoizeInCreateSelector() { const createSelectorMicroMemoize = createSelectorCreator({ memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b }, + memoizeOptions: [{ isEqual: (a, b) => a === b }], + // memoizeOptions: { isEqual: (a, b) => a === b }, argsMemoize: microMemoize, argsMemoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicroMemoize = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType(selectorMicroMemoize(state)) @@ -278,7 +371,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { selectorMicroMemoize.dependencies expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -305,7 +398,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // the options parameter of `defaultMemoize`, the output selector fields // also change their type to the return type of `defaultMemoize`. const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -340,7 +433,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -362,7 +455,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) // Making sure the type behavior is consistent when args are passed in as an array. const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { memoize: defaultMemoize, @@ -397,7 +490,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -420,20 +513,22 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, memoizeOptions: { isPromise: false, - resultEqualityCheck: (a: unknown, b: unknown) => a === b + resultEqualityCheck: + // @ts-expect-error + (a, b) => a === b }, argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } } ) const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, @@ -466,7 +561,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -493,7 +588,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -527,7 +622,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -553,32 +648,70 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ]) ) - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, - // `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - const selectorMicroMemoizePartiallyOverridden = createSelectorMicroMemoize( - (state: State) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { - // @ts-expect-error - equalityCheck: (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) + const selectorMicroMemoizePartiallyOverridden = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden1 = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden2 = + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + // memoizeOptions: [ + // { + // equalityCheck: + // // @ts-expect-error + // (a, b) => a === b, + // maxSize: 2 + // } + // ], + argsMemoizeOptions: [{ isPromise: false }] + } + ) const selectorDefaultParametric = createSelector( - (state: State, id: number) => id, - (state: State) => state.todos, + (state: RootState, id: number) => id, + (state: RootState) => state.todos, (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, inputStabilityCheck: 'never', memoize: memoizeOne, + argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] } ) @@ -623,8 +756,8 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State, id: number) => number, - (state: State) => { id: number; completed: boolean }[] + (state: RootState, id: number) => number, + (state: RootState) => { id: number; completed: boolean }[] ] >(selectorDefaultParametric.dependencies) expectExactType<{ id: number; completed: boolean }[]>( @@ -657,7 +790,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { }) const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = createSelectorArgsMemoizeOptionsFallbackToDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType( @@ -688,7 +821,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -725,8 +858,11 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b }, - // @ts-expect-error - argsMemoizeOptions: { isEqual: (a, b) => a === b } + argsMemoizeOptions: { + isEqual: + // @ts-expect-error implicit any + (a, b) => a === b + } }) // When passing in an options object as the first argument, there should be no other arguments. @@ -776,6 +912,24 @@ function deepNesting() { selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].memoizedResultFunc.cache + 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) + selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache } function deepNesting1() { @@ -908,3 +1062,121 @@ function deepNesting2() { memoize: defaultMemoize }) } + +function parameterLimit() { + const selector = createSelector( + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + ( + foo1: string, + foo2: number, + foo3: boolean, + foo4: string, + foo5: string, + foo6: string, + foo7: string, + foo8: number, + foo9: string[], + foo10: string, + foo11: number, + foo12: boolean, + foo13: string, + foo14: string, + foo15: string, + foo16: string, + foo17: number, + foo18: string[], + foo19: string, + foo20: number, + foo21: boolean, + foo22: string, + foo23: string, + foo24: string, + foo25: string, + foo26: number, + foo27: string[], + foo28: string, + foo29: number, + foo30: boolean, + foo31: string, + foo32: string, + foo33: string, + foo34: string, + foo35: number, + foo36: string[] + ) => { + return { + foo1, + foo2, + foo3, + foo4, + foo5, + foo6, + foo7, + foo8, + foo9, + foo10, + foo11, + foo12, + foo13, + foo14, + foo15, + foo16, + foo17, + foo18, + foo19, + foo20, + foo21, + foo22, + foo23, + foo24, + foo25, + foo26, + foo27, + foo28, + foo29, + foo30, + foo31, + foo32, + foo33, + foo34, + foo35, + foo36 + } + } + ) +} diff --git a/typescript_test/test.ts b/typescript_test/test.ts index c6be7e511..7f674d320 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -9,7 +9,7 @@ import type { TypedUseSelectorHook } from 'react-redux' import { useSelector } from 'react-redux' import type { GetStateFromSelectors, - ParametricSelector, + Selector, SelectorResultArray, TypedStructuredSelectorCreator } from 'reselect' @@ -20,10 +20,7 @@ import { defaultEqualityCheck, defaultMemoize } from 'reselect' - -export function expectType(t: T): T { - return t -} +import { expectExactType } from './typesTestUtils' type Exact = (() => T extends A ? 1 : 0) extends () => T extends B ? 1 @@ -35,25 +32,6 @@ type Exact = (() => T extends A ? 1 : 0) extends () => T extends B : never : never -export declare type IsAny = true | false extends ( - T extends never ? true : false -) - ? True - : False - -export declare type IsUnknown = unknown extends T - ? IsAny - : False - -type Equals = IsAny< - T, - never, - IsAny -> -export function expectExactType(t: T) { - return >(u: U) => {} -} - interface StateA { a: number } @@ -164,7 +142,7 @@ function testSelectorAsCombiner() { type Component

= (props: P) => any declare function connect( - selector: ParametricSelector + selector: Selector ): (component: Component

) => Component

function testConnect() { @@ -852,14 +830,14 @@ function testTypedCreateStructuredSelector() { const selectBar = (state: RootState) => state.bar const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = - createStructuredSelector as TypedStructuredSelectorCreator + createStructuredSelector typedStructuredSelectorCreator({ foo: selectFoo, bar: selectBar }) - // @ts-expect-error + // @ts-expect-error Because `bar` is missing. typedStructuredSelectorCreator({ foo: selectFoo }) diff --git a/typescript_test/typesTestUtils.ts b/typescript_test/typesTestUtils.ts new file mode 100644 index 000000000..90d0d5b23 --- /dev/null +++ b/typescript_test/typesTestUtils.ts @@ -0,0 +1,23 @@ +export function expectType(t: T): T { + return t +} + +export declare type IsAny = true | false extends ( + T extends never ? true : false +) + ? True + : False + +export declare type IsUnknown = unknown extends T + ? IsAny + : False + +type Equals = IsAny< + T, + never, + IsAny +> + +export function expectExactType(t: T) { + return >(u: U) => {} +} diff --git a/vitest.config.ts b/vitest.config.ts index 0ee99ef30..dad9aad5b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { + typecheck: { tsconfig: './type-tests/tsconfig.json' }, globals: true, include: ['./test/**/*.(spec|test).[jt]s?(x)'], alias: { diff --git a/yarn.lock b/yarn.lock index 2ba7fc9a9..5f3be759d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -850,6 +850,26 @@ __metadata: languageName: node linkType: hard +"@typescript/analyze-trace@npm:^0.10.1": + version: 0.10.1 + resolution: "@typescript/analyze-trace@npm:0.10.1" + dependencies: + chalk: ^4.1.2 + exit: ^0.1.2 + jsonparse: ^1.3.1 + jsonstream-next: ^3.0.0 + p-limit: ^3.1.0 + split2: ^3.2.2 + treeify: ^1.1.0 + yargs: ^16.2.0 + bin: + analyze-trace: bin/analyze-trace + print-trace-types: bin/print-trace-types + simplify-trace-types: bin/simplify-trace-types + checksum: 967cad7eeedfd4c9e1a89f94a1613b81711237567a5f0061b242691a20dbe4d67b8dd5c50b4ec884165a324029b681529ff08edbcb8bf96b68e4cc5df52f1ce1 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -1252,7 +1272,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -1304,6 +1324,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^7.0.0 + checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1734,6 +1765,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.1": + version: 3.1.1 + resolution: "escalade@npm:3.1.1" + checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 + languageName: node + linkType: hard + "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -1936,6 +1974,13 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2128,6 +2173,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -2435,7 +2487,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2": +"inherits@npm:2, inherits@npm:^2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -2811,6 +2863,25 @@ __metadata: languageName: node linkType: hard +"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d + languageName: node + linkType: hard + +"jsonstream-next@npm:^3.0.0": + version: 3.0.0 + resolution: "jsonstream-next@npm:3.0.0" + dependencies: + jsonparse: ^1.2.0 + through2: ^4.0.2 + bin: + jsonstream-next: bin.js + checksum: 651d9d304ae9b23e397f0c1c60d8679daab41f42981eca6eaa61527a2f249d1cc0e8f3b3da2ce686590933ee92301d2092cb4cc65a24206d5a4e4409b77bdc21 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -2928,9 +2999,11 @@ __metadata: linkType: hard "lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.3 - resolution: "lru-cache@npm:10.0.3" - checksum: e4b100c5a6b2ac778c0f63711499b5098686205c57907d8c04a413270d37089112d9bd0192dfa36940eb5d94b88c7db54fdb6fd23319c8f89903cfd4323ea06c + version: 10.0.2 + resolution: "lru-cache@npm:10.0.2" + dependencies: + semver: ^7.3.5 + checksum: 83ad0e899d79f48574bdda131fe8157c6d65cbd073a6e78e0d1a3467a85dce1ef4d8dc9fd618a56c57a068271501c81d54471e13f84dd121e046b155ed061ed4 languageName: node linkType: hard @@ -3335,7 +3408,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -3636,6 +3709,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:3, readable-stream@npm:^3.0.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -3711,6 +3795,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + "requireindex@npm:~1.1.0": version: 1.1.0 resolution: "requireindex@npm:1.1.0" @@ -3735,6 +3826,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.1.0 "@typescript-eslint/eslint-plugin-tslint": 5.1.0 "@typescript-eslint/parser": 5.1.0 + "@typescript/analyze-trace": ^0.10.1 eslint: ^8.0.1 eslint-plugin-react: ^7.26.1 eslint-plugin-typescript: 0.14.0 @@ -3929,6 +4021,13 @@ __metadata: languageName: node linkType: hard +"safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + "safe-regex-test@npm:^1.0.0": version: 1.0.0 resolution: "safe-regex-test@npm:1.0.0" @@ -4111,6 +4210,15 @@ __metadata: languageName: node linkType: hard +"split2@npm:^3.2.2": + version: 3.2.2 + resolution: "split2@npm:3.2.2" + dependencies: + readable-stream: ^3.0.0 + checksum: 8127ddbedd0faf31f232c0e9192fede469913aa8982aa380752e0463b2e31c2359ef6962eb2d24c125bac59eeec76873678d723b1c7ff696216a1cd071e3994a + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -4134,7 +4242,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -4206,6 +4314,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: ~5.2.0 + checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4320,6 +4437,15 @@ __metadata: languageName: node linkType: hard +"through2@npm:^4.0.2": + version: 4.0.2 + resolution: "through2@npm:4.0.2" + dependencies: + readable-stream: 3 + checksum: ac7430bd54ccb7920fd094b1c7ff3e1ad6edd94202e5528331253e5fde0cc56ceaa690e8df9895de2e073148c52dfbe6c4db74cacae812477a35660090960cc0 + languageName: node + linkType: hard + "tinybench@npm:^2.5.0": version: 2.5.1 resolution: "tinybench@npm:2.5.1" @@ -4368,6 +4494,13 @@ __metadata: languageName: node linkType: hard +"treeify@npm:^1.1.0": + version: 1.1.0 + resolution: "treeify@npm:1.1.0" + checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -4581,6 +4714,13 @@ __metadata: languageName: node linkType: hard +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + "vite-node@npm:0.34.6": version: 0.34.6 resolution: "vite-node@npm:0.34.6" @@ -4807,7 +4947,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -4836,6 +4976,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -4850,6 +4997,28 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"