diff --git a/README.md b/README.md index 0c4e6518..91916717 100644 --- a/README.md +++ b/README.md @@ -1,686 +1,1830 @@ # Reselect +![TypeScript][typescript-badge][![npm package][npm-badge]][npm][![Coveralls][coveralls-badge]][coveralls][![GitHub Workflow Status][build-badge]][build] + A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. -- Selectors can compute derived data, allowing Redux to store the minimal possible state. -- Selectors are efficient. A selector is not recomputed unless one of its arguments changes. -- Selectors are composable. They can be used as input to other selectors. +- Selectors can compute derived data, allowing Redux to store the minimal possible state. +- Selectors are efficient. A selector is not recomputed unless one of its arguments changes. +- Selectors are composable. They can be used as input to other selectors. + +The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical `Reselect` usage patterns, and using selectors with React-Redux. + +## Installation + +### Redux Toolkit + +While `Reselect` is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. + +```ts +import { createSelector } from '@reduxjs/toolkit' +``` + +### Standalone + +For standalone usage, install the `Reselect` package: + +#### Using `npm` + +```bash +npm install reselect +``` + +#### Using `yarn` + +```bash +yarn add reselect +``` + +#### Using `bun` + +```bash +bun add reselect +``` + +#### Using `pnpm` + +```bash +pnpm add reselect +``` + +--- + +## Basic Usage + +`Reselect` exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more `input selectors`, which extract values from arguments, and a `combiner` function that receives the extracted values and should return a derived value. If the generated `output selector` is called multiple times, the output will only be recalculated when the extracted values have changed. + +You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/reselect-example-g3k9gf?file=/src/index.js): + +```ts +import { createSelector } from 'reselect' + +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 } + ] +} + +const selectCompletedTodos = (state: RootState) => { + console.log('selector ran') + return state.todos.filter(todo => todo.completed === true) +} + +selectCompletedTodos(state) // selector ran +selectCompletedTodos(state) // selector ran +selectCompletedTodos(state) // selector ran + +const memoizedSelectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + console.log('memoized selector ran') + return todos.filter(todo => todo.completed === true) + } +) + +memoizedSelectCompletedTodos(state) // memoized selector ran +memoizedSelectCompletedTodos(state) +memoizedSelectCompletedTodos(state) + +console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false + +console.log( + memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state) +) //=> true +``` + +As you can see from the example above, `memoizedSelectCompletedTodos` does not run the second or third time, but we still get the same return value as last time. + +Another difference is that with `memoizedSelectCompletedTodos` the referential integrity of the return value is also maintained through multiple calls of the selector, but the same cannot be said about the first example. + +--- + +## Table of Contents + +- [Installation](#installation) + - [Redux Toolkit](#redux-toolkit) + - [Standalone](#standalone) +- [Basic Usage](#basic-usage) +- [API](#api) + - [**`createSelector`**](#createselector) + - [**`defaultMemoize`**](#defaultmemoize) + - [**`weakMapMemoize`**](#weakmapmemoize) + - [**`autotrackMemoize`**](#autotrackmemoize) + - [**`createSelectorCreator`**](#createselectorcreator) + - [**`createStructuredSelector`**](#createstructuredselector) + - [**`createCurriedSelector`**](#createcurriedselector) +- [Debugging Tools](#debuggingtools) +- [FAQ](#faq) + - [Why isn’t my selector recomputing when the input state changes?](#why-isnt-my-selector-recomputing-when-the-input-state-changes) + - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) + - [Can I use Reselect without Redux?](#can-i-use-reselect-without-redux) + - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) + - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) + - [How do I test a selector?](#how-do-i-test-a-selector) + - [Can I share a selector across multiple component instances?](#can-i-share-a-selector-across-multiple-component-instances) + - [Are there TypeScript Typings?](#are-there-typescript-typings) + - [I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite`](#i-am-seeing-a-typescript-error-type-instantiation-is-excessively-deep-and-possibly-infinite) + - [How can I make a curried selector?](#how-can-i-make-a-curried-selector) +- [Related Projects](#related-projects) +- [License](#license) +- [Prior Art and Inspiration](#prior-art-and-inspiration) + +--- + +
+ + + +## Terminology + + + +- [**`Selector Function`**](#selector-function): Any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state. +- [**`Input Selectors`**](#input-selectors): Standard selector functions that are used to create the result function. +- [**`Output Selector`**](#output-selector): The actual selectors generated by calling `createSelector`. +- [**`Result Function`**](#result-function): The function that comes after the input selectors. It takes input selectors' return values as arguments and returns a result. Otherwise known as the `combiner`. +- [**`Combiner`**](#combiner): A function that takes input selectors' return values as arguments and returns a result. This term is somewhat interchangeably used with `resultFunc`. But `combiner` is more of a general term and `resultFunc` is more specific to `Reselect`. So the `resultFunc` is a `combiner` but a `combiner` is not necessarily the same as `resultFunc`. +- [**`Dependencies`**](#dependencies): Same as input selectors. They are what the output selector "depends" on. + +The below example serves as a visual aid. + +```ts +const outputSelector = createSelector( + [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`. + combiner // synonymous with `Result Function` or `resultFunc`. +) +``` + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + +## How Does `Reselect` Work? + + + +### Cascading Memoization + + + +The way `Reselect` works can be broken down into multiple parts: + +1. **Initial Run**: On the first call, `Reselect` runs all the `input selectors`, gathers their results, and passes them to the `result function`. + +2. **Subsequent Runs**: For subsequent calls, `Reselect` performs two levels of checks: + + - **First Level**: It compares the current arguments with the previous ones. + + - If they're the same, it returns the cached result without running the `input selectors` or the `result function`. + + - If they differ, it proceeds to the second level. + + - **Second Level**: It runs the `input selectors` and compares their current results with the previous ones. + > [!NOTE] + > If any one of the `input selectors` return a different result, all `input selectors` will recalculate. + - If the results are the same, it returns the cached result without running the `result function`. + - If the results differ, it runs the `result function`. + +This behavior is what we call **_Cascading Double-Layer Memoization_**. + + + +### `Reselect` Vs Standard Memoization + +##### Standard Memoization + +![normal-memoization-function](docs/assets//normal-memoization-function.png) + +_Standard memoization only compares arguments. If they're the same, it returns the cached result._ + +##### Memoization with `Reselect` + +![reselect-memoization](docs/assets//reselect-memoization.png) + +_`Reselect` adds a second layer of checks with the `input selectors`. This is crucial in `Redux` applications where state references change frequently._ + +A normal memoization function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. `Reselect`'s behavior differs from a simple memoization function in that, it adds a second layer of checks through the `input selectors`. So what can sometimes happen is that the arguments that get passed to the `input selectors` change, but the result of the `input selectors` still remain the same, and that means we can still skip running the `result function`. + +This is especially important since in a `Redux` application, your `state` is going to change its reference any time you make an update through dispatched actions. + +> [!NOTE] +> The `input selectors` take the same arguments as the `output selector`. + +#### Why `Reselect` Is Often Used With `Redux` + +Imagine you have a selector like this: + +```ts +const selectCompletedTodos = (state: RootState) => + state.todos.filter(todo => todo.completed === true) +``` + +So you decide to memoize it: + +```ts +const selectCompletedTodos = someMemoizeFunction((state: RootState) => + state.todos.filter(todo => todo.completed === true) +) +``` + +Then you update `state.alerts`: + +```ts +store.dispatch(toggleRead(0)) +``` + +Now when you call `selectCompletedTodos`, it re-runs, because we have effectively broken memoization. + +```ts +selectCompletedTodos(store.getState()) +selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +store.dispatch(toggleRead(0)) +selectCompletedTodos(store.getState()) // It recalculates. +``` + +But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in `Redux` anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. + +But with `Reselect`, we can do something like this: + +```ts +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true) +) +``` + +And now we have achieved memoization: + +```ts +selectCompletedTodos(store.getState()) +selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +store.dispatch(toggleRead(0)) +selectCompletedTodos(store.getState()) // The `input selectors` will run, But the `result function` is skipped and the cached result will be returned. +``` + +Even though our `state` changes, and we fail the comparison check on the arguments, the `result function` will not run because the current `state.todos` is still the same as the previous `state.todos`. So we have at least partial memoization, because the memoization "cascades" and goes from the arguments to the results of the `input selectors`. So basically there are "2 layers of checks" and in this situation, while the first one fails, the second one succeeds. And that is why this type of `Cascading Double-Layer Memoization` makes `Reselect` a good candidate to be used with `Redux`. + +
[ ↑ Back to top ↑ ]
+ +--- + +## API + + + +### createSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) + +
+ + + +#### Description + + + +Accepts one or more ["input selectors"](#input-selectors) (either as separate arguments or a single array), +a single ["result function"](#result-function) / ["combiner"](#combiner), and an optional options object, and +generates a memoized selector function. + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | +| `inputSelectors` | An array of [`input selectors`](#input-selectors), can also be passed as separate arguments. | +| `combiner` | A [`combiner`](#combiner) function that takes the results of the [`input selectors`](#input-selectors) as separate arguments. | +| `createSelectorOptions?` | An optional options object that allows for further customization per selector. | + +
+ +
+ + + +#### Returns + + + +A memoized [`output selector`](#output-selector). + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InputSelectors` | The type of the [`input selectors`](#input-selectors) array. | +| `Result` | The return type of the [`combiner`](#combiner) as well as the [`output selector`](#output-selector). | +| `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. | +| `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. | + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + +### Memoization Functions + + + +#### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) + +
+ + + +##### Description + + + +The standard memoize function used by `createSelector`. + +It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (**`Since`** 4.1.0). + +It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [`reference equality`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality): + +```ts +const defaultEqualityCheck = (previousValue: any, currentValue: any) => { + return previousValue === currentValue +} +``` + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----------------------- | :---------------------------------------------------------- | +| `func` | The function to be memoized. | +| `equalityCheckOrOptions` | Either an `equality check` function or an `options` object. | + +**`Since`** 4.1.0, `defaultMemoize` also accepts an options object as its first argument instead of an `equalityCheck` function. The `options` object may contain: + +```ts +type EqualityFn = (a: any, b: any) => boolean + +interface DefaultMemoizeOptions { + equalityCheck?: EqualityFn + resultEqualityCheck?: EqualityFn + maxSize?: number +} +``` + +| Name | Description | +| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `equalityCheck` | Used to compare the individual arguments of the provided calculation function.
**`Default`** = `defaultEqualityCheck` | +| `resultEqualityCheck` | If provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This addresses the common todos.map(todo => todo.id) use case, where an update to another field in the original data causes a recalculation due to changed references, but the output is still effectively the same. | +| `maxSize` | The cache size for the selector. If greater than 1, the selector will use an LRU cache internally.
**`Default`** = 1 | + +> [!WARNING] +> If `resultEqualityCheck` is used inside `argsMemoizeOptions` it has no effect. + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +###### Using `createSelector` + +```ts +import { shallowEqual } from 'react-redux' +import { createSelector } from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { + memoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + }, + argsMemoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + } + } +) +``` + +###### Using `createSelectorCreator` + +```ts +import { shallowEqual } from 'react-redux' +import { createSelectorCreator, defaultMemoize } from 'reselect' + +const createSelectorShallowEqual = createSelectorCreator({ + memoize: defaultMemoize, + memoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + }, + argsMemoize: defaultMemoize, + argsMemoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + } +}) + +const selectTodoIds = createSelectorShallowEqual( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + + + +#### weakMapMemoize(func) - (**`Since`** 5.0.0) + +
+ + + +##### Description + + + +`defaultMemoize` has to be explicitly configured to have a cache size larger than 1, and uses an LRU cache internally. + +`weakMapMemoize` creates a tree of `WeakMap`-based cache nodes based on the identity of the arguments it's been called with (in this case, the extracted values from your input selectors). **This allows `weakMapMemoize` to have an effectively infinite cache size**. Cache results will be kept in memory as long as references to the arguments still exist, and then cleared out as the arguments are garbage-collected. + +**Design Tradeoffs for `weakMapMemoize`:** + +- Pros: + - It has an effectively infinite cache size, but you have no control over + how long values are kept in cache as it's based on garbage collection and `WeakMap`s. +- Cons: + - There's currently no way to alter the argument comparisons. They're based on strict reference equality. + +**Use Cases for `weakMapMemoize`:** + +- This memoizer is likely best used for cases where you need to call the + same selector instance with many different arguments, such as a single + selector instance that is used in a list item component and called with + item IDs like: + +```ts +useSelector(state => selectSomeData(state, id)) +``` + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. | + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +Prior to `weakMapMemoize`, you had this problem: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) // Selector runs again! +``` + +Before you could solve this in a number of different ways: + +1. Set the `maxSize` with `defaultMemoize`: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id), + { + memoizeOptions: { + maxSize: 10 + } + } +) +``` + +But this required having to know the cache size ahead of time. + +2. Create unique selector instances using `useMemo`. + +```tsx +const parametricSelector = (id: number) => + createSelector([(state: RootState) => state.todos], todos => + todos.filter(todo => todo.id === id) + ) + +const MyComponent: FC = ({ id }) => { + const selectTodosById = useMemo(() => parametricSelector(id), [id]) + + const todosById = useSelector(selectTodosById) + + return ( +
+ {todosById.map(todo => ( +
{todo.title}
+ ))} +
+ ) +} +``` + +3. Using `useCallback`. + +```tsx +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +const MyComponent: FC = ({ id }) => { + const selectTodosById = useCallback(parametricSelector, []) + + const todosById = useSelector(state => selectTodosById(state, id)) + + return ( +
+ {todosById.map(todo => ( +
{todo.title}
+ ))} +
+ ) +} +``` + +4. Use `re-reselect`: + +```ts +import { createCachedSelector } from 're-reselect' + +const parametricSelector = createCachedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +)((state: RootState, id: number) => id) +``` + +Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. + +###### Using `createSelector` + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id), + { + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize + } +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) +``` + +###### Using `createSelectorCreator` + +```ts +import { createSelectorCreator, weakMapMemoize } from 'reselect' + +const createSelectorWeakMap = createSelectorCreator({ + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize +}) + +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) +``` + +This solves the problem of having to know and set the cache size prior to creating a memoized selector. + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + + + +#### autotrackMemoize(func) - (**`Since`** 5.0.0) + +
+ + + +##### Description + + + +Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. It uses a Proxy to wrap arguments and track accesses to nested fields in your selector on first read. Later, when the selector is called with new arguments, it identifies which accessed fields have changed and only recalculates the result if one or more of those accessed fields have changed. This allows it to be more precise than the shallow equality checks in defaultMemoize. + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. | + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +###### Using `createSelector` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector +} from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { memoize: autotrackMemoize } +) +``` + +###### Using `createSelectorCreator` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator +} from 'reselect' + +const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize +}) + +const selectTodoIds = createSelectorAutotrack( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +**Design Tradeoffs for autotrackMemoize:** + +- Pros: + - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. +- Cons: + + - It only has a cache size of 1. + - It is slower than defaultMemoize, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc) + - It can have some unexpected behavior. Because it tracks nested field accesses, cases where you don't access a field will not recalculate properly. For example, a badly-written selector like: + + ```ts + createSelector([state => state.todos], todos => todos) + ``` + + that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check. + +**Use Cases for `autotrackMemoize`:** + +- It is likely best used for cases where you need to access specific nested fields in data, and avoid recalculating if other fields in the same data objects are immutably updated. + +Using `createSelector` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector +} from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { memoize: autotrackMemoize } +) +``` + +Using `createSelectorCreator` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator +} from 'reselect' + +const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize +}) + +const selectTodoIds = createSelectorAutotrack( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + + + +### createSelectorCreator(memoize | options, ...memoizeOptions) + +
+ + + +#### Description + + + +Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or **`Since`** 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. + +
+ +
+ + + +#### Parameters (**`Since`** 5.0.0) + + + +| Name | Description | +| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the `output selector` generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
**`Since`** 5.0.0 | +| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
**`Since`** 5.0.0 | +| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | +| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
**`Since`** 5.0.0 | + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. | + +
+ +
+ + + +#### Returns + + + +A customized `createSelector` function. + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | + +
+ +
+ + + +#### **`Examples`** + + + +##### Using `options` (**`Since`** 5.0.0) + +```ts +const customCreateSelector = createSelectorCreator({ + memoize: customMemoize, // Function to be used to memoize `resultFunc` + memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards + argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments + argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards +}) + +const customSelector = customCreateSelector( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as the first argument to `customMemoize` +) + +customSelector( + ...selectorArgs // Will be memoized by `customArgsMemoize` +) +``` + +
[ ↑ Back to top ↑ ]
+ +--- + +##### Using `memoize` and `...memoizeOptions` + +`createSelectorCreator` can be used to make a customized version of `createSelector`. + +The `memoize` argument is a memoization function to replace `defaultMemoize`. + +The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: + +```ts +const customSelectorCreator = createSelectorCreator( + customMemoize, // Function to be used to memoize `resultFunc` + option1, // `option1` will be passed as second argument to `customMemoize` + option2, // `option2` will be passed as third argument to `customMemoize` + option3 // `option3` will be passed as fourth argument to `customMemoize` +) + +const customSelector = customSelectorCreator( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as first argument to `customMemoize` +) +``` + +Internally `customSelector` calls the memoize function as follows: + +```ts +customMemoize(resultFunc, option1, option2, option3) +``` + +##### Additional Examples + +###### Customize `equalityCheck` for `defaultMemoize` + +```js +import { createSelectorCreator, defaultMemoize } from 'reselect' +import isEqual from 'lodash.isequal' + +// create a "selector creator" that uses lodash.isequal instead of === +const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) + +// use the new "selector creator" to create a selector +const selectSum = createDeepEqualSelector( + [state => state.values.filter(val => val < 5)], + values => values.reduce((acc, val) => acc + val, 0) +) +``` + +###### Use memoize function from `Lodash` for an unbounded cache + +```js +import { createSelectorCreator } from 'reselect' +import memoize from 'lodash.memoize' + +const hashFn = (...args) => + args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') + +const customSelectorCreator = createSelectorCreator(memoize, hashFn) + +const selector = customSelectorCreator( + [state => state.a, state => state.b], + (a, b) => a + b +) +``` + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + + + +### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) + +
+ + + +#### Description + + + +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. + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :----------------- | :------------------------------------------------------------------- | +| `selectorMap` | A key value pair consisting of input selectors. | +| `selectorCreator?` | A custom selector creator function. It defaults to `createSelector`. | + +
+ +
+ + + +#### Returns + + + +A memoized structured selector. + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `InputSelectorsObject` | The shape of the `input selectors` object. | +| `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | +| `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. | + +
+ +
+ + + +#### **`Examples`** + + + +##### Modern Use Case + +```ts +import { createSelector, createStructuredSelector } from 'reselect' + +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 } + ] +} + +// This: +const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: (state: RootState, id: number) => state.todos[id] + }, + createSelector +) + +// Is essentially the same as this: +const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + (state: RootState, id: number) => state.todos[id] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } +) +``` + +##### Simple Use Case + +```ts +const selectA = state => state.a +const selectB = state => state.b + +// The result function in the following selector +// is simply building an object from the input selectors +const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + a, + b +})) + +const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } +``` + +
+ +
[ ↑ Back to top ↑ ]
+ +--- + + + +## Debugging Tools + +
+ + + + + +### Development-only Checks - (**`Since`** 5.0.0) + + + + + +#### `inputStabilityCheck` + +Due to how [`Cascading Memoization`](#cascadingmemoization) works in `Reselect`, it is crucial that your `input selectors` do not return a new reference on each run. If an `input selector` always returns a new reference, like + +```ts +state => ({ a: state.a, b: state.b }) +``` + +or + +```ts +state => state.todos.map(todo => todo.id) +``` + +that will cause the selector to never memoize properly. +Since this is a common mistake, we've added a development mode check to catch this. By default, `createSelector` will now run the `input selectors` twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. + +```ts +type StabilityCheckFrequency = 'always' | 'once' | 'never' +``` + +| Possible Values | Description | +| :-------------- | :---------------------------------------------- | +| `once` | Run only the first time the selector is called. | +| `always` | Run every time the selector is called. | +| `never` | Never run the `input stability check`. | + +> [!IMPORTANT] +> The `input stability check` is automatically disabled in production environments. + +You can configure this behavior in two ways: + + + +##### 1. Globally through `setInputStabilityCheckEnabled`: + +A `setInputStabilityCheckEnabled` function is exported from `Reselect`, which should be called with the desired setting. + +```ts +import { setInputStabilityCheckEnabled } from 'reselect' + +// Run only the first time the selector is called. (default) +setInputStabilityCheckEnabled('once') + +// Run every time the selector is called. +setInputStabilityCheckEnabled('always') + +// Never run the input stability check. +setInputStabilityCheckEnabled('never') +``` + +##### 2. Per selector by passing an `inputStabilityCheck` option directly to `createSelector`: -The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with React-Redux. +```ts +// Create a selector that double-checks the results of `input selectors` every time it runs. +const selectCompletedTodosLength = createSelector( + [ + // This `input selector` will not be memoized properly since it always returns a new reference. + (state: RootState) => + state.todos.filter(({ completed }) => completed === true) + ], + completedTodos => completedTodos.length, + // Will override the global setting. + { inputStabilityCheck: 'always' } +) +``` -[![GitHub Workflow Status][build-badge]][build] -[![npm package][npm-badge]][npm] -[![Coveralls][coveralls-badge]][coveralls] +> [!WARNING] +> This will override the global `input stability check` set by calling `setInputStabilityCheckEnabled`. -## Installation +
-### Redux Toolkit +
-While Reselect is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. + -```js -import { createSelector } from '@reduxjs/toolkit' -``` + -### Standalone +### Output Selector Fields -For standalone usage, install the `reselect` package: + -```bash -npm install reselect +The `output selectors` created by `createSelector` have several additional properties attached to them: -yarn add reselect -``` +| Name | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resultFunc` | The final function passed to `createSelector`. Otherwise known as the `combiner`. | +| `memoizedResultFunc` | The memoized version of `resultFunc`. | +| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | +| `dependencies` | The array of the input selectors used by `createSelector` to compose the combiner (`memoizedResultFunc`). | +| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | +| `resetRecomputations` | Resets the count of `recomputations` count to 0. | +| `dependencyRecomputations` | Counts the number of times the input selectors (`dependencies`) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the result function. | +| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | +| `memoize` | Function used to memoize the `resultFunc`. | +| `argsMemoize` | Function used to memoize the arguments passed into the `output selector`. | -## Basic Usage +
-Reselect exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more "input" selectors, which extract values from arguments, and an "output" selector that receives the extracted values and should return a derived value. If the generated selector is called multiple times, the output will only be recalculated when the extracted values have changed. +
[ ↑ Back to top ↑ ]
-You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/objective-waterfall-1z5y8?file=/src/index.js): +--- -```js -import { createSelector } from 'reselect' +
-const selectShopItems = state => state.shop.items -const selectTaxPercent = state => state.shop.taxPercent + -const selectSubtotal = createSelector(selectShopItems, items => - items.reduce((subtotal, item) => subtotal + item.value, 0) -) + -const selectTax = createSelector( - selectSubtotal, - selectTaxPercent, - (subtotal, taxPercent) => subtotal * (taxPercent / 100) -) +## What's New in 5.0.0? -const selectTotal = createSelector( - selectSubtotal, - selectTax, - (subtotal, tax) => ({ total: subtotal + tax }) -) + -const exampleState = { - shop: { - taxPercent: 8, - items: [ - { name: 'apple', value: 1.2 }, - { name: 'orange', value: 0.95 } - ] - } -} +Version 5.0.0 introduces several new features and improvements: -console.log(selectSubtotal(exampleState)) // 2.15 -console.log(selectTax(exampleState)) // 0.172 -console.log(selectTotal(exampleState)) // { total: 2.322 } -``` +- **Customization Enhancements**: -## Table of Contents + - Added the ability to pass an options object to `createSelectorCreator`, allowing for customized `memoize` and `argsMemoize` functions, alongside their respective options (`memoizeOptions` and `argsMemoizeOptions`). + - The `createSelector` function now supports direct customization of `memoize` and `argsMemoize` within its options object. -- [Installation](#installation) - - [Redux Toolkit](#redux-toolkit) - - [Standalone](#standalone) -- [Basic Usage](#basic-usage) -- [API](#api) - - [createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)](#createselectorinputselectors--inputselectors-resultfunc-selectoroptions) - - [defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck)](#defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck) - - [createSelectorCreator(memoize, ...memoizeOptions)](#createselectorcreatormemoize-memoizeoptions) - - [Customize `equalityCheck` for `defaultMemoize`](#customize-equalitycheck-for-defaultmemoize) - - [Use memoize function from Lodash for an unbounded cache](#use-memoize-function-from-lodash-for-an-unbounded-cache) - - [createStructuredSelector({inputSelectors}, selectorCreator = createSelector)](#createstructuredselectorinputselectors-selectorcreator--createselector) -- [Development-only checks](#development-only-checks) - - [`inputStabilityCheck`](#inputstabilitycheck) - - [Global configuration](#global-configuration) - - [Per-selector configuration](#per-selector-configuration) -- [FAQ](#faq) - - [Q: Why isn’t my selector recomputing when the input state changes?](#q-why-isnt-my-selector-recomputing-when-the-input-state-changes) - - [Q: Why is my selector recomputing when the input state stays the same?](#q-why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - - [Q: Can I use Reselect without Redux?](#q-can-i-use-reselect-without-redux) - - [Q: How do I create a selector that takes an argument?](#q-how-do-i-create-a-selector-that-takes-an-argument) - - [Q: The default memoization function is no good, can I use a different one?](#q-the-default-memoization-function-is-no-good-can-i-use-a-different-one) - - [Q: How do I test a selector?](#q-how-do-i-test-a-selector) - - [Q: Can I share a selector across multiple component instances?](#q-can-i-share-a-selector-across-multiple-component-instances) - - [Q: Are there TypeScript Typings?](#q-are-there-typescript-typings) - - [Q: How can I make a curried selector?](#q-how-can-i-make-a-curried-selector) -- [Related Projects](#related-projects) - - [re-reselect](#re-reselect) - - [reselect-tools](#reselect-tools) - - [reselect-debugger](#reselect-debugger) -- [License](#license) -- [Prior Art and Inspiration](#prior-art-and-inspiration) +- **Memoization Functions**: -## API + - Introduced new experimental memoization functions: `weakMapMemoize` and `autotrackMemoize`. + - Incorporated `memoize` and `argsMemoize` into the [`output selector fields`](#outputselectorfields) for debugging purposes. -### createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?) +- **TypeScript Support and Performance**: -Accepts one or more "input selectors" (either as separate arguments or a single array), a single "output selector" / "result function", and an optional options object, and generates a memoized selector function. + - Discontinued support for `TypeScript` versions below 4.7, aligning with modern `TypeScript` features. + - Significantly improved `TypeScript` performance for nesting `output selectors`. The nesting limit has increased from approximately 8 to around 30 `output selectors`, greatly reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. -When the selector is called, each input selector will be called with all of the provided arguments. The extracted values are then passed as separate arguments to the output selector, which should calculate and return a final result. The inputs and result are cached for later use. +- **Selector API Enhancements**: -If the selector is called again with the same arguments, the previously cached result is returned instead of recalculating a new result. + - Introduced experimental APIs: `createCurriedSelector` and `createCurriedSelectorCreator`, for more advanced selector patterns. + - Removed the second overload of `createStructuredSelector` due to its susceptibility to runtime errors. + - Added the `TypedStructuredSelectorCreator` utility type (_currently a work-in-progress_) to facilitate the creation of a pre-typed version of `createStructuredSelector` for your root state. -`createSelector` determines if the value returned by an input-selector has changed between calls using reference equality (`===`). Inputs to selectors created with `createSelector` should be immutable. +- **Additional Functionalities**: -By default, selectors created with `createSelector` have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector. This can be customized by passing a `selectorOptions` object with a `memoizeOptions` field containing options for the built-in `defaultMemoize` memoization function . + - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the `output selector fields`. These additions provide greater control and insight over `input selectors`, complementing the new `argsMemoize` API. + - Introduced `inputStabilityCheck`, a development tool that runs the `input selectors` twice using the same arguments and triggers a warning If they return differing results for the same call. -```js -const selectValue = createSelector( - state => state.values.value1, - state => state.values.value2, - (value1, value2) => value1 + value2 -) +These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. -// You can also pass an array of selectors -const selectTotal = createSelector( - [state => state.values.value1, state => state.values.value2], - (value1, value2) => value1 + value2 -) +
-// Selector behavior can be customized -const customizedSelector = createSelector( - state => state.a, - state => state.b, - (a, b) => a + b, - { - // New in 4.1: Pass options through to the built-in `defaultMemoize` function - memoizeOptions: { - equalityCheck: (a, b) => a === b, - maxSize: 10, - resultEqualityCheck: shallowEqual - } +
[ ↑ Back to top ↑ ]
+ +--- + +
+ + + + + +## Optimizing `Reselect` + + + +### Empty Array Pattern + +To reduce recalculations, use a predefined empty array when `array.filter` or similar methods result in an empty array. + +So you can have a pattern like this: + +```ts +const EMPTY_ARRAY = [] + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + const completedTodos = todos.filter(todo => todo.completed === true) + return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos } ) ``` -Selectors are typically called with a Redux `state` value as the first argument, and the input selectors extract pieces of the `state` object for use in calculations. However, it's also common to want to pass additional arguments, such as a value to filter by. Since input selectors are given all arguments, they can extract the additional arguments and pass them to the output selector: +Or to avoid repetition, you can create a wrapper function and reuse it: -```js -const selectItemsByCategory = createSelector( - [ - // Usual first input - extract value from `state` - state => state.items, - // Take the second arg, `category`, and forward to the output selector - (state, category) => category - ], - // Output selector gets (`items, category)` as args - (items, category) => items.filter(item => item.category === category) +```ts +const EMPTY_ARRAY = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } ) ``` -### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) - -`defaultMemoize` memoizes the function passed in the func parameter. It is the standard memoize function used by `createSelector`. +There are a few details that will help you skip running as many functions as possible and get the best possible performance out of `Reselect`: -`defaultMemoize` has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (new in 4.1). +- Due to the `Cascading Memoization` in `Reselect`, The first layer of checks is upon the arguments that are passed to the `output selector`, therefore it's best to maintain the same reference for the arguments as much as possible. +- In `Redux`, your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, try to avoid passing in objects or arrays and mostly stick to primitives like numbers for ids. +- Keep your [`input selectors`](#input-selectors) as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside `input selectors`, and you should definitely not be returning an object or array with a new reference each time. +- The `result function` is only re-run as a last resort. So make sure to put any and all calculations inside your `result function`. That way, `Reselect` will only run those calculations if all other checks fail. -`defaultMemoize` determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using reference equality: +This: -```js -function defaultEqualityCheck(previousVal, currentVal) { - return currentVal === previousVal -} +```ts +const selectorGood = createSelector( + [(state: RootState) => state.todos], + todos => someExpensiveComputation(todos) +) ``` -As of Reselect 4.1, `defaultMemoize` also accepts an options object as its first argument instead of `equalityCheck`. The options object may contain: +Is preferable to this: ```ts -interface DefaultMemoizeOptions { - equalityCheck?: EqualityFn - resultEqualityCheck?: EqualityFn - maxSize?: number -} +const selectorBad = createSelector( + [(state: RootState) => someExpensiveComputation(state.todos)], + someOtherCalculation +) ``` -Available options are: +Because we have less calculations in input selectors and more in the result function. -- `equalityCheck`: used to compare the individual arguments of the provided calculation function -- `resultEqualityCheck`: if provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This address the common `todos.map(todo => todo.id)` use case, where an update to another field in the original data causes a recalculate due to changed references, but the output is still effectively the same. -- `maxSize`: the cache size for the selector. If `maxSize` is greater than 1, the selector will use an LRU cache internally +
-The returned memoized function will have a `.clearCache()` method attached. +
[ ↑ Back to top ↑ ]
-`defaultMemoize` can also be used with `createSelectorCreator` to create a new selector factory that always has the same settings for each selector. +--- -### createSelectorCreator(memoize, ...memoizeOptions) +## FAQ -`createSelectorCreator` can be used to make a customized version of `createSelector`. +
-The `memoize` argument is a memoization function to replace `defaultMemoize`. + -The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: +### Why isn’t my selector recomputing when the input state changes? -```js -const customSelectorCreator = createSelectorCreator( - customMemoize, // function to be used to memoize resultFunc - option1, // option1 will be passed as second argument to customMemoize - option2, // option2 will be passed as third argument to customMemoize - option3 // option3 will be passed as fourth argument to customMemoize -) + -const customSelector = customSelectorCreator( - input1, - input2, - resultFunc // resultFunc will be passed as first argument to customMemoize -) -``` +A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using `Redux`). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using `Redux`, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). -Internally `customSelector` calls the memoize function as follows: +
-```js -customMemoize(resultFunc, option1, option2, option3) -``` +
-Here are some examples of how you might use `createSelectorCreator`: + -#### Customize `equalityCheck` for `defaultMemoize` +### Why is my selector recomputing when the input state stays the same? -```js -import { createSelectorCreator, defaultMemoize } from 'reselect' -import isEqual from 'lodash.isequal' +A: Make sure you have `inputStabilityCheck` set to either `always` or `once` and that in and of itself should take some weight off of your shoulders by doing some of the debugging for you. Also make sure you use your `output selector fields` like `recomputations`, `resetRecomputations`, `dependencyRecomputations`, `resetDependencyRecomputations` to narrow down the root of the problem to see where it is coming from. Is it coming from the arguments changing reference unexpectedly? then if that is the case your `dependencyRecomputations` should be going up. If `dependencyRecomputations` is incrementing but `recomputations` is not, that means your arguments are changing reference too often. If your `input selectors` return a new reference every time, that will be caught be `inputStabilityCheck`. And if your arguments are changing reference too often, you can narrow it down to see which arguments are changing reference too often by doing this: -// create a "selector creator" that uses lodash.isequal instead of === -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) +```ts +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean; type: string }[] +} -// use the new "selector creator" to create a selector -const selectSum = createDeepEqualSelector( - state => state.values.filter(val => val < 5), - values => values.reduce((acc, val) => acc + val, 0) +const selectAlertsByType = createSelector( + [ + (state: RootState) => state.alerts, + (state: RootState, type: string) => type + ], + (alerts, type) => alerts.filter(todo => todo.type === type), + { + argsMemoizeOptions: { + // This will check the arguments passed to the `output selector`. + equalityCheck: (a, b) => { + if (a !== b) { + console.log(a, 'is not equal to', b) + } + return a === b + } + } + } ) ``` -#### Use memoize function from Lodash for an unbounded cache + -```js -import { createSelectorCreator } from 'reselect' -import memoize from 'lodash.memoize' +
-let called = 0 -const hashFn = (...args) => - args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') -const customSelectorCreator = createSelectorCreator(memoize, hashFn) -const selector = customSelectorCreator( - state => state.a, - state => state.b, - (a, b) => { - called++ - return a + b - } -) -``` +
-### createStructuredSelector({inputSelectors}, selectorCreator = createSelector) + -`createStructuredSelector` is 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: +### Can I use `Reselect` without `Redux`? -```js -const selectA = state => state.a -const selectB = state => state.b + -// The result function in the following selector -// is simply building an object from the input selectors -const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ - a, - b -})) -``` +A: Yes. `Reselect` has no dependencies on any other package, so although it was designed to be used with `Redux` it can be used independently. It can be used with any plain JS data, such as typical `React` state values, as long as that data is being updated immutably. -`createStructuredSelector` takes an object whose properties are input-selectors and returns a structured selector. The structured selector returns an object with the same keys as the `inputSelectors` argument, but with the selectors replaced with their values. +
-```js -const selectA = state => state.a -const selectB = state => state.b +
-const structuredSelector = createStructuredSelector({ - x: selectA, - y: selectB -}) + -const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } -``` +### How do I create a selector that takes an argument? -Structured selectors can be nested: +When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and `output selectors` appropriately. Here are key points to consider: -```js -const nestedSelector = createStructuredSelector({ - subA: createStructuredSelector({ - selectorA, - selectorB - }), - subB: createStructuredSelector({ - selectorC, - selectorD - }) -}) -``` +1. **Consistency in Arguments**: Ensure that all positional arguments across `input selectors` are of the same type for consistency. -## Development-only checks +2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all `input selectors` receive the same arguments that are passed to the `output selector`. -### `inputStabilityCheck` + + +Suppose we have the following state structure: + +```ts +interface RootState { + items: { id: number; category: string }[] + // ... other state properties ... +} ``` -By default, this will only happen when the selector is first called. You can configure the check globally or per selector, to change it to always run when the selector is called, or to never run. +To create a selector that filters `items` based on a `category` and excludes a specific `id`, you can set up your selectors as follows: -_This check is disabled for production environments._ +```ts +const selectAvailableItems = createSelector( + [ + // First input selector extracts items from the state + (state: RootState) => state.items, + // Second input selector forwards the category argument + (state: RootState, category: string) => category, + // Third input selector forwards the ID argument + (state: RootState, category: string, id: number) => id + ], + // Output selector uses the extracted items, category, and ID + (items, category, id) => + items.filter(item => item.category === category && item.id !== id) +) +``` -#### Global configuration +Internally `Reselect` is doing this: -A `setInputStabilityCheckEnabled` function is exported from `reselect`, which should be called with the desired setting. +```ts +// Input selector #1 +const items = (state: RootState, category: string, id: number) => state.items +// Input selector #2 +const category = (state: RootState, category: string, id: number) => category +// Input selector #3 +const id = (state: RootState, category: string, id: number) => id +// result of `output selector` +const finalResult = + // The `Result Function` + items.filter(item => item.category === category && item.id !== id) +``` -```js -import { setInputStabilityCheckEnabled } from 'reselect' + -// run when selector is first called (default) -setInputStabilityCheckEnabled('once') +
-// always run -setInputStabilityCheckEnabled('always') +
-// never run -setInputStabilityCheckEnabled('never') -``` + -#### Per-selector configuration +### The default memoization function is no good, can I use a different one? -A value can be passed as part of the selector options object, which will override the global setting for the given selector. + -```ts -const selectPersonName = createSelector( - selectPerson, - person => person.firstName + ' ' + person.lastName, - // `inputStabilityCheck` accepts the same settings - // as `setInputStabilityCheckEnabled` - { inputStabilityCheck: 'never' } -) -``` +A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). -## FAQ +
-### Q: Why isn’t my selector recomputing when the input state changes? +
-A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using Redux, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). + -The following example defines a simple selector that determines if the first todo item in an array of todos has been completed: +### How do I test a selector? -```js -const selectIsFirstTodoComplete = createSelector( - state => state.todos[0], - todo => todo && todo.completed -) -``` +A: For a given input, a selector should always produce the same result. For this reason they are simple to unit test. -The following state update function **will not** work with `selectIsFirstTodoComplete`: +```ts +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} -```js -export default function todos(state = initialState, action) { - switch (action.type) { - case COMPLETE_ALL: - const areAllMarked = state.every(todo => todo.completed) - // BAD: mutating an existing object - return state.map(todo => { - todo.completed = !areAllMarked - return todo - }) - - default: - return state - } +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] } + +// With `Vitest` +test('selector unit test', () => { + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const firstResult = selectTodoIds(state) + const secondResult = selectTodoIds(state) + // Reference equality should pass. + expect(firstResult).toBe(secondResult) + // Deep equality should also pass. + expect(firstResult).toStrictEqual(secondResult) + selectTodoIds(state) + selectTodoIds(state) + selectTodoIds(state) + // The `Result Function` should not recalculate. + expect(selectTodoIds.recomputations()).toBe(1) + // `Input selectors` should not recalculate. + expect(selectTodoIds.dependencyRecomputations()).toBe(1) +}) ``` -The following state update function **will** work with `selectIsFirstTodoComplete`: + -```js -export default function todos(state = initialState, action) { - switch (action.type) { - case COMPLETE_ALL: - const areAllMarked = state.every(todo => todo.completed) - // GOOD: returning a new object each time with Object.assign - return state.map(todo => - Object.assign({}, todo, { - completed: !areAllMarked - }) - ) - - default: - return state - } -} -``` +
-If you are not using Redux and have a requirement to work with mutable data, you can use `createSelectorCreator` to replace the default memoization function and/or use a different equality check function. See [here](#use-memoize-function-from-lodash-for-an-unbounded-cache) and [here](#customize-equalitycheck-for-defaultmemoize) for examples. +
-### Q: Why is my selector recomputing when the input state stays the same? + -A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` that recomputes unexpectedly may be receiving a new object on each update whether the values it contains have changed or not. `createSelector` uses an identity check (`===`) to detect that an input has changed, so returning a new object on each update means that the selector will recompute on each update. +### Can I share a selector across multiple component instances? -```js -import { REMOVE_OLD } from '../constants/ActionTypes' + -const initialState = [ - { - text: 'Use Redux', - completed: false, - id: 0, - timestamp: Date.now() - } -] - -export default function todos(state = initialState, action) { - switch (action.type) { - case REMOVE_OLD: - return state.filter(todo => { - return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now() - }) - default: - return state - } -} -``` +A: Yes, as of 5.0.0 you can use `weakMapMemoize` to achieve this. -The following selector is going to recompute every time REMOVE_OLD is invoked because Array.filter always returns a new object. However, in the majority of cases the REMOVE_OLD action will not change the list of todos so the recomputation is unnecessary. +
-```js -import { createSelector } from 'reselect' +
-const todosSelector = state => state.todos + -export const selectVisibleTodos = createSelector( - todosSelector, - (todos) => { - ... - } -) -``` +### Are there TypeScript Typings? -You can eliminate unnecessary recomputations by returning a new object from the state update function only when a deep equality check has found that the list of todos has actually changed: + -```js -import { REMOVE_OLD } from '../constants/ActionTypes' -import isEqual from 'lodash.isequal' +A: Yes! `Reselect` is now written in `TypeScript` itself, so they should Just Work™. -const initialState = [ - { - text: 'Use Redux', - completed: false, - id: 0, - timestamp: Date.now() - } -] - -export default function todos(state = initialState, action) { - switch (action.type) { - case REMOVE_OLD: - const updatedState = state.filter(todo => { - return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now() - }) - return isEqual(updatedState, state) ? state : updatedState - default: - return state - } -} -``` +
-Alternatively, the default `equalityCheck` function in the selector can be replaced by a deep equality check: +
-```js -import { createSelectorCreator, defaultMemoize } from 'reselect' -import isEqual from 'lodash.isequal' + -const selectTodos = state => state.todos +### I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` -// create a "selector creator" that uses lodash.isequal instead of === -const createDeepEqualSelector = createSelectorCreator( - defaultMemoize, - isEqual -) + -// use the new "selector creator" to create a selector -const mySelector = createDeepEqualSelector( - todosSelector, - (todos) => { - ... - } -) -``` +A: **`Since`** 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this +comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as +relating to nested selectors. -Always check that the cost of an alternative `equalityCheck` function or deep equality check in the state update function is not greater than the cost of recomputing every time. If recomputing every time does work out to be the cheaper option, it may be that for this case Reselect is not giving you any benefit over passing a plain `mapStateToProps` function to `connect`. +
-### Q: Can I use Reselect without Redux? +
-A: Yes. Reselect has no dependencies on any other package, so although it was designed to be used with Redux it can be used independently. It can be used with any plain JS data, such as typical React state values, as long as that data is being updated immutably. + -### Q: How do I create a selector that takes an argument? +### How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? -As shown in the API reference section above, provide input selectors that extract the arguments and forward them to the output selector for calculation: + -```js -const selectItemsByCategory = createSelector( - [ - // Usual first input - extract value from `state` - state => state.items, - // Take the second arg, `category`, and forward to the output selector - (state, category) => category - ], - // Output selector gets (`items, category)` as args - (items, category) => items.filter(item => item.category === category) -) -``` +A: You can try this new experimental API: -### Q: The default memoization function is no good, can I use a different one? + -A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). +#### createCurriedSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) -### Q: How do I test a selector? +This: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) -A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test. +parametricSelector(state, 0) +``` -```js -const selector = createSelector( - state => state.a, - state => state.b, - (a, b) => ({ - c: a * 2, - d: b * 3 - }) +Is the same as this: + +```ts +const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) ) -test('selector unit test', () => { - assert.deepEqual(selector({ a: 1, b: 2 }), { c: 2, d: 6 }) - assert.deepEqual(selector({ a: 2, b: 3 }), { c: 4, d: 9 }) -}) +curriedSelector(0)(state) ``` -It may also be useful to check that the memoization function for a selector works correctly with the state update function (i.e. the reducer if you are using Redux). Each selector has a `recomputations` method that will return the number of times it has been recomputed: +As before you had to do this: -```js -suite('selector', () => { - let state = { a: 1, b: 2 } - - const reducer = (state, action) => ({ - a: action(state.a), - b: action(state.b) - }) - - const selector = createSelector( - state => state.a, - state => state.b, - (a, b) => ({ - c: a * 2, - d: b * 3 - }) - ) +```ts +const selectTodo = useSelector(state => parametricSelector(state, props.id)) +``` - const plusOne = x => x + 1 - const id = x => x - - test('selector unit test', () => { - state = reducer(state, plusOne) - assert.deepEqual(selector(state), { c: 4, d: 9 }) - state = reducer(state, id) - assert.deepEqual(selector(state), { c: 4, d: 9 }) - assert.equal(selector.recomputations(), 1) - state = reducer(state, plusOne) - assert.deepEqual(selector(state), { c: 6, d: 12 }) - assert.equal(selector.recomputations(), 2) - }) -}) +Now you can do this: + +```ts +const selectTodo = useSelector(curriedSelector(props.id)) ``` -Additionally, selectors keep a reference to the last result function as `.resultFunc`. If you have selectors composed of many other selectors this can help you test each selector without coupling all of your tests to the shape of your state. +Another thing you can do if you are using `React-Redux` is this: -For example if you have a set of selectors like this: +```ts +import type { GetParamsFromSelectors, Selector } from 'reselect' +import { useSelector } from 'react-redux' + +export const createParametricSelectorHook = ( + selector: S +) => { + return (...args: GetParamsFromSelectors<[S]>) => { + return useSelector(state => selector(state, ...args)) + } +} -**selectors.js** +const useSelectTodo = createParametricSelectorHook(parametricSelector) +``` -```js -export const selectFirst = createSelector( ... ) -export const selectSecond = createSelector( ... ) -export const selectThird = createSelector( ... ) +And then inside your component: -export const myComposedSelector = createSelector( - selectFirst, - selectSecond, - selectThird, - (first, second, third) => first * second < third -) +```tsx +import type { FC } from 'react' + +interface Props { + id: number +} + +const MyComponent: FC = ({ id }) => { + const todo = useSelectTodo(id) + return
{todo.title}
+} ``` -And then a set of unit tests like this: +### How can I make pre-typed version of `createSelector` for my root state? -**test/selectors.js** +A: You can create a custom typed version of `createSelector` by defining a utility type that extends the original `createSelector` function. Here's an example: -```js -// tests for the first three selectors... -test("selectFirst unit test", () => { ... }) -test("selectSecond unit test", () => { ... }) -test("selectThird unit test", () => { ... }) - -// We have already tested the previous -// three selector outputs so we can just call `.resultFunc` -// with the values we want to test directly: -test("myComposedSelector unit test", () => { - // here instead of calling selector() - // we just call selector.resultFunc() - assert(myComposedSelector.resultFunc(1, 2, 3), true) - assert(myComposedSelector.resultFunc(2, 2, 1), false) -}) +```ts +import type { createSelector, SelectorsArray, Selector } from 'reselect' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export type TypedCreateSelector = < + SelectorsArray extends readonly Selector[], + Result +>( + ...createSelectorArgs: Parameters< + typeof createSelector + > +) => ReturnType> + +export const createAppSelector: TypedCreateSelector = createSelector ``` -Finally, each selector has a `resetRecomputations` method that sets -recomputations back to 0. The intended use is for a complex selector that may -have many independent tests and you don't want to manually manage the -computation count or create a "dummy" selector for each test. +> [!WARNING]: This approach currently only supports `input selectors` provided as a single array. -### Q: Can I share a selector across multiple component instances? +
-A: Yes, although it requires some planning. +
[ ↑ Back to top ↑ ]
-As of Reselect 4.1, you can create a selector with a cache size greater than one by passing in a `maxSize` option under `memoizeOptions` for use with the built-in `defaultMemoize`. +--- -Otherwise, selectors created using `createSelector` only have a cache size of one. This can make them unsuitable for sharing across multiple instances if the arguments to the selector are different for each instance of the component. There are a couple of ways to get around this: +
-- Create a factory function which returns a new selector for each instance of the component. This can be called in a React component inside the `useMemo` hook to generate a unique selector instance per component. -- Create a custom selector with a cache size greater than one using `createSelectorCreator` + -### Q: Are there TypeScript Typings? +## External References -A: Yes! Reselect is now written in TS itself, so they should Just Work™. + -### Q: I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` +- [**`WeakMap`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) +- [**`Reference Equality Check`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality) -A: This can often occur with deeply recursive types, which occur in this library. Please see [this -comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as -relating to nested selectors. +
-### Q: How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? +
-A: Try these [helper functions](https://github.com/reduxjs/reselect/issues/159#issuecomment-238724788) courtesy of [MattSPalmer](https://github.com/MattSPalmer) + ## Related Projects + + ### [re-reselect](https://github.com/toomuchdesign/re-reselect) -Enhances Reselect selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. +Enhances `Reselect` selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments. @@ -696,7 +1840,7 @@ Useful to reduce selectors recalculation when the same selector is repeatedly ca [Flipper plugin](https://github.com/vlanemcev/flipper-plugin-reselect-debugger) and [and the connect app](https://github.com/vlanemcev/reselect-debugger-flipper) for debugging selectors in **React Native Apps**. -Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper. +Inspired by `Reselect Tools`, so it also has all functionality from this library and more, but only for React Native and Flipper. - Selectors Recomputations count in live time across the App for identify performance bottlenecks - Highlight most recomputed selectors @@ -706,17 +1850,36 @@ Inspired by Reselect Tools, so it also has all functionality from this library a - Selectors Output (In case if selector not dependent from external arguments) - Shows "Not Memoized (NM)" selectors +
+ +
[ ↑ Back to top ↑ ]
+ +--- + ## License MIT +
+ + + ## Prior Art and Inspiration + + Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nuclear-js.git), [subscriptions](https://github.com/Day8/re-frame#just-a-read-only-cursor) in [re-frame](https://github.com/Day8/re-frame) and this [proposal](https://github.com/reduxjs/redux/pull/169) from [speedskater](https://github.com/speedskater). -[build-badge]: https://img.shields.io/github/workflow/status/reduxjs/redux-thunk/Tests +[typescript-badge]: https://img.shields.io/badge/TypeScript-v4%2E7%2B-007ACC?style=for-the-badge&logo=TypeScript&logoColor=black&labelColor=blue&color=gray +[build-badge]: https://img.shields.io/github/actions/workflow/status/reduxjs/reselect/build-and-test-types.yml?branch=master&style=for-the-badge [build]: https://github.com/reduxjs/reselect/actions/workflows/build-and-test-types.yml -[npm-badge]: https://img.shields.io/npm/v/reselect.svg?style=flat-square +[npm-badge]: https://img.shields.io/npm/v/reselect.svg?style=for-the-badge [npm]: https://www.npmjs.org/package/reselect -[coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=flat-square +[coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=for-the-badge [coveralls]: https://coveralls.io/github/reduxjs/reselect + +
+ +
[ ↑ Back to top ↑ ]
+ +--- diff --git a/docs/assets/diagrams/normal-memoization-function.drawio b/docs/assets/diagrams/normal-memoization-function.drawio new file mode 100644 index 00000000..479edfd6 --- /dev/null +++ b/docs/assets/diagrams/normal-memoization-function.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/diagrams/reselect-memoization.drawio b/docs/assets/diagrams/reselect-memoization.drawio new file mode 100644 index 00000000..3bdab150 --- /dev/null +++ b/docs/assets/diagrams/reselect-memoization.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/normal-memoization-function.png b/docs/assets/normal-memoization-function.png new file mode 100644 index 00000000..bffa8ede Binary files /dev/null and b/docs/assets/normal-memoization-function.png differ diff --git a/docs/assets/reselect-memoization.png b/docs/assets/reselect-memoization.png new file mode 100644 index 00000000..f31afb83 Binary files /dev/null and b/docs/assets/reselect-memoization.png differ diff --git a/package.json b/package.json index 8ca71686..a2e4a8d1 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,10 @@ "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", "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" }, "keywords": [ diff --git a/src/autotrackMemoize/autotrackMemoize.ts b/src/autotrackMemoize/autotrackMemoize.ts index e5d698a0..f2a0a179 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], @@ -93,7 +97,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/createCurriedSelectorCreator.ts b/src/createCurriedSelectorCreator.ts new file mode 100644 index 00000000..f842e6ee --- /dev/null +++ b/src/createCurriedSelectorCreator.ts @@ -0,0 +1,132 @@ +import type { CreateSelectorFunction } from './createSelectorCreator' +import { createSelectorCreator } from './createSelectorCreator' + +import { defaultMemoize } from './defaultMemoize' +import type { + Combiner, + CreateSelectorOptions, + CurriedOutputSelector, + DropFirstParameter, + InterruptRecursion, + SelectorArray, + Simplify, + UnknownMemoizer +} from './types' + +/** + * @WIP + */ +export interface CreateCurriedSelector< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> { + /** + * One arg + */ + ( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > & + InterruptRecursion + + /** + * inline args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner, + createSelectorOptions: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion + + /** + * array args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + inputSelectors: [...InputSelectors], + combiner: Combiner, + createSelectorOptions?: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion +} + +/** + * @WIP + */ +export function createCurriedSelectorCreator< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +>(...createSelectorCreatorArgs: Parameters) { + const createSelector = createSelectorCreator(...createSelectorCreatorArgs) + + const createCurriedSelector = ( + ...createSelectorArgs: Parameters< + CreateSelectorFunction + > + ) => { + // @ts-ignore + const selector = createSelector.apply(null, createSelectorArgs) + // const selector = createSelector(...createSelectorArgs) + const curriedSelector = selector.argsMemoize( + (...params: DropFirstParameter) => { + return selector.argsMemoize((state: Parameters[0]) => { + return selector(state, ...params) + }) + } + ) + return Object.assign(curriedSelector, selector) as CurriedOutputSelector + } + return createCurriedSelector as unknown as CreateCurriedSelector< + MemoizeFunction, + ArgsMemoizeFunction + > +} + +/** + * @WIP + */ +export const createCurriedSelector = + /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 85525ca9..ff9d4623 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 42cfe293..be926cc2 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -80,12 +80,12 @@ export interface StructuredSelectorCreator { * ```ts * import { createSelector, createStructuredSelector } from 'reselect' * - * interface State { + * interface RootState { * todos: { id: number; completed: boolean }[] * alerts: { id: number; read: boolean }[] * } * - * const state: State = { + * const state: RootState = { * todos: [ * { id: 0, completed: false }, * { id: 1, completed: true } @@ -99,9 +99,9 @@ export interface StructuredSelectorCreator { * // This: * const structuredSelector = createStructuredSelector( * { - * allTodos: (state: State) => state.todos, - * allAlerts: (state: State) => state.alerts, - * selectedTodo: (state: State, id: number) => state.todos[id] + * allTodos: (state: RootState) => state.todos, + * allAlerts: (state: RootState) => state.alerts, + * selectedTodo: (state: RootState, id: number) => state.todos[id] * }, * createSelector * ) @@ -109,9 +109,9 @@ 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) => { * return { diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index a5c87335..716c8a4d 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 @@ -209,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 9ad07492..ab800d4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,9 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' +export { + createCurriedSelector, + createCurriedSelectorCreator +} from './createCurriedSelectorCreator' +export type { CreateCurriedSelector } from './createCurriedSelectorCreator' export { createSelector, createSelectorCreator, @@ -15,6 +20,7 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, + DefaultMemoizeFields, EqualityFn, ExtractMemoizerFields, GetParamsFromSelectors, diff --git a/src/types.ts b/src/types.ts index c50128c7..276070c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,12 +93,12 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { memoize: weakMapMemoize } * ) * ``` @@ -106,23 +106,27 @@ export interface CreateSelectorOptions< * @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 selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { argsMemoize: weakMapMemoize } * ) * ``` @@ -187,19 +191,58 @@ 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. */ + + /** + * Resets the count of {@linkcode OutputSelectorFields.recomputations recomputations} count to 0. + * + */ resetRecomputations: () => 0 + + /** + * 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: () => 0 } & Simplify< Required< Pick< @@ -237,6 +280,27 @@ export type OutputSelector< ArgsMemoizeFunction > + +export type Curried any> = ( + ...params: DropFirstParameter +) => (state: Parameters[0]) => ReturnType + +export type CurriedOutputSelector< + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> = Curried< + OutputSelector +> & + ExtractMemoizerFields & + OutputSelectorFields< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > + /** * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. * @@ -335,11 +399,11 @@ export type MemoizeOptionsFromParameters< MemoizeFunction extends UnknownMemoizer > = | ( - | Simplify[0]>> + | NonFunctionType[0]> | FunctionType[0]> ) | ( - | Simplify[number]>> + | NonFunctionType[number]> | FunctionType[number]> )[] @@ -358,12 +422,12 @@ export type OverrideMemoizeOptions< OverrideMemoizeFunction extends UnknownMemoizer = never > = IfNever< OverrideMemoizeFunction, - MemoizeOptionsFromParameters, - MemoizeOptionsFromParameters + Simplify>, + Simplify> > /** - * Extracts the additional fields that a memoize function attaches to + * Extracts the additional properties or methods that a memoize function attaches to * the function it memoizes (e.g., `clearCache`). * * @template MemoizeFunction - The type of the memoize function to be checked. @@ -373,6 +437,25 @@ export type OverrideMemoizeOptions< 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 +} + /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- @@ -414,7 +497,9 @@ export type FallbackIfNever = IfNever * * @internal */ -export type NonFunctionType = OmitIndexSignature> +export type NonFunctionType = Simplify< + OmitIndexSignature> +> /** * Extracts the function part of a type. @@ -466,11 +551,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 +563,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 +649,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 +698,20 @@ 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> + /** * * ----------------------------------------------------------------------------- @@ -717,9 +817,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 ceabd235..54579edf 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/weakMapMemoize.ts b/src/weakMapMemoize.ts index 5311c907..6f5b079f 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,12 +87,12 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { memoize: weakMapMemoize } * ) * ``` @@ -78,14 +102,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 selectTodosById = createSelectorWeakMap( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id] + * (todos, id) => todos.filter(todo => todo.id === id) * ) * ``` * @@ -96,14 +120,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' || @@ -151,5 +173,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 12d4a14c..bdabb689 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 } @@ -32,6 +35,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateA) => state.a, a => a ) + selector.memoizedResultFunc.clearCache const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } const secondState = { a: 2 } diff --git a/test/benchmarks/createCurriedSelector.bench.ts b/test/benchmarks/createCurriedSelector.bench.ts new file mode 100644 index 00000000..428e6f6d --- /dev/null +++ b/test/benchmarks/createCurriedSelector.bench.ts @@ -0,0 +1,53 @@ +import { createCurriedSelector, createSelector } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +describe.only('curriedSelector vs parametric selector', () => { + const options: Options = { + // iterations: 10_000_000, + // time: 0 + } + const store = setupStore() + const state = store.getState() + const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + setFunctionNames({ parametricSelector, curriedSelector }) + bench( + parametricSelector, + () => { + parametricSelector(state, 0) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + parametricSelector.clearCache() + parametricSelector.resetRecomputations() + parametricSelector.memoizedResultFunc.clearCache() + } + } + ) + bench( + curriedSelector, + () => { + curriedSelector(0)(state) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + curriedSelector.clearCache() + curriedSelector.resetRecomputations() + curriedSelector.memoizedResultFunc.clearCache() + } + } + ) +}) diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts new file mode 100644 index 00000000..db156ba3 --- /dev/null +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -0,0 +1,117 @@ +import type { Selector } from 'reselect' +import { createSelector } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + logRecomputations, + setFunctionNames, + setupStore, + toggleCompleted +} from '../testUtils' + +describe.only('less in input selectors vs more in input selectors', () => { + const store = setupStore() + const state = store.getState() + const arr = Array.from({ length: 1_000_000 }, (e, i) => i) + const runSelector = (selector: Selector) => { + arr.forEach((e, i) => { + selector(store.getState(), 0) + }) + arr.forEach((e, i) => { + selector(store.getState(), 0) + }) + } + + const selectorGood = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) + const selectorBad = createSelector( + [ + (state: RootState, id: number) => state.todos.find(todo => todo.id === id) + ], + todo => todo?.completed + ) + + let called = 0 + const nonMemoized = (state: RootState, id: number) => { + called++ + return state.todos.find(todo => todo.id === id)?.completed + } + const options: Options = { + // warmupIterations: 0, + // warmupTime: 0, + iterations: 10, + time: 0 + } + setFunctionNames({ selectorGood, selectorBad, nonMemoized }) + const createOptions = < + S extends Selector & { + recomputations: () => number + dependencyRecomputations: () => number + } + >( + selector: S + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + // store.dispatch(toggleRead(0)) + }, + afterAll: () => { + logRecomputations(selector) + } + } + } + } + return options + } + bench( + selectorGood, + () => { + selectorGood(store.getState(), 0) + }, + { + ...options, + ...createOptions(selectorGood) + } + ) + bench( + selectorBad, + () => { + selectorBad(store.getState(), 0) + }, + { + ...options, + ...createOptions(selectorBad) + } + ) + bench( + nonMemoized, + () => { + nonMemoized(store.getState(), 0) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') { + called = 0 + return + } + task.opts = { + beforeEach: () => { + // store.dispatch(toggleRead(0)) + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + console.log(`${nonMemoized.name} called:`, called, `time(s)`) + } + } + } + } + ) +}) diff --git a/test/benchmarks/reselect.bench.ts b/test/benchmarks/reselect.bench.ts new file mode 100644 index 00000000..0b69bd46 --- /dev/null +++ b/test/benchmarks/reselect.bench.ts @@ -0,0 +1,292 @@ +import { + createSelector, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +const options: Options = { + // iterations: 10_000_000, + // time: 0 +} + +describe.skip('bench', () => { + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: RootState) => { + return state.todos.map(({ id }) => id) + } + + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id], + { argsMemoize: weakMapMemoize } + ) + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'selectorAutotrack', + () => { + selectorAutotrack(state) + }, + options + ) + bench( + 'selectorWeakMap', + () => { + selectorWeakMap(state) + }, + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options + ) +}) + +describe.skip('for loops', () => { + const store = setupStore() + const state = store.getState() + const { todos } = state + const { length } = todos + bench( + 'for loop length not cached', + () => { + for (let i = 0; i < todos.length; i++) { + // + todos[i].completed + todos[i].id + } + }, + options + ) + bench( + 'for loop length cached', + () => { + for (let i = 0; i < length; i++) { + // + todos[i].completed + todos[i].id + } + }, + options + ) + bench( + 'for loop length and arg cached', + () => { + for (let i = 0; i < length; i++) { + // + const arg = todos[i] + arg.completed + arg.id + } + }, + options + ) +}) + +describe.skip('nested field access', () => { + const store = setupStore() + const state = store.getState() + 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 + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + options + ) +}) + +describe.skip('simple field access', () => { + const store = setupStore() + const state = store.getState() + 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 + ) + const selectorDefault2 = createSelector( + (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) => 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, + users => users.user.details.preferences.notifications.push.frequency + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + options + ) + bench( + 'selectorDefault2', + () => { + selectorDefault2(state) + }, + options + ) +}) + +describe.only('field accessors', () => { + const store = setupStore() + const selectorDefault = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const nonMemoizedSelector = (state: RootState) => state.users.appSettings + + setFunctionNames({ selectorDefault, nonMemoizedSelector }) + + const options: Options = { + // iterations: 1000, + // time: 0 + } + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + { ...options } + ) + bench( + nonMemoizedSelector, + () => { + nonMemoizedSelector(store.getState()) + }, + { ...options } + ) +}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts new file mode 100644 index 00000000..f3d52363 --- /dev/null +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -0,0 +1,442 @@ +import type { Selector } from 'reselect' +import { + createSelector, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +import type { Options } from 'tinybench' + +const store = setupStore() +const state = store.getState() +const arr = Array.from({ length: 30 }, (e, i) => i) + +const options: Options = { + // iterations: 100_000, + // time: 0 +} + +describe.only('weakMapMemoize vs defaultMemoize', () => { + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id) + ) + const selectorDefaultWithCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithArgsCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithBothCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { memoize: autotrackMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const nonMemoizedSelector = (state: RootState, id: number) => { + return state.todos.map(todo => todo.id === id) + } + setFunctionNames({ + selectorDefault, + selectorDefaultWithCacheSize, + selectorDefaultWithArgsCacheSize, + selectorDefaultWithBothCacheSize, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap, + selectorAutotrack, + selectorArgsAutotrack, + selectorBothAutotrack, + nonMemoizedSelector + }) + const runSelector = (selector: Selector) => { + arr.forEach((e, i) => { + selector(state, e) + }) + arr.forEach((e, i) => { + selector(state, e) + }) + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefault.clearCache() + selectorDefault.resetRecomputations() + selectorDefault.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefault.name} recomputations after:`, + selectorDefault.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithCacheSize, + () => { + runSelector(selectorDefaultWithCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithCacheSize.clearCache() + selectorDefaultWithCacheSize.resetRecomputations() + selectorDefaultWithCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithCacheSize.name} recomputations after:`, + selectorDefaultWithCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithArgsCacheSize, + () => { + runSelector(selectorDefaultWithArgsCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithArgsCacheSize.clearCache() + selectorDefaultWithArgsCacheSize.resetRecomputations() + selectorDefaultWithArgsCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithArgsCacheSize.name} recomputations after:`, + selectorDefaultWithArgsCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithBothCacheSize, + () => { + runSelector(selectorDefaultWithBothCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithBothCacheSize.clearCache() + selectorDefaultWithBothCacheSize.resetRecomputations() + selectorDefaultWithBothCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithBothCacheSize.name} recomputations after:`, + selectorDefaultWithBothCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorWeakMap.clearCache() + selectorWeakMap.resetRecomputations() + selectorWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorWeakMap.name} recomputations after:`, + selectorWeakMap.recomputations() - 1, + selectorWeakMap.dependencyRecomputations() + ) + } + } + } + } + ) + bench( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorArgsWeakMap.clearCache() + selectorArgsWeakMap.resetRecomputations() + selectorArgsWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorArgsWeakMap.name} recomputations after:`, + selectorArgsWeakMap.recomputations() - 1, + selectorArgsWeakMap.dependencyRecomputations() + ) + } + } + } + } + ) + bench( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorBothWeakMap.clearCache() + selectorBothWeakMap.resetRecomputations() + selectorBothWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorBothWeakMap.name} recomputations after:`, + selectorBothWeakMap.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorAutotrack, + () => { + runSelector(selectorAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorAutotrack.clearCache() + selectorAutotrack.resetRecomputations() + selectorAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorAutotrack.name} recomputations after:`, + selectorAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorArgsAutotrack, + () => { + runSelector(selectorArgsAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorArgsAutotrack.clearCache() + selectorArgsAutotrack.resetRecomputations() + selectorArgsAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorArgsAutotrack.name} recomputations after:`, + selectorArgsAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorBothAutotrack, + () => { + runSelector(selectorBothAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorBothAutotrack.clearCache() + selectorBothAutotrack.resetRecomputations() + selectorBothAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorBothAutotrack.name} recomputations after:`, + selectorBothAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + nonMemoizedSelector, + () => { + runSelector(nonMemoizedSelector) + }, + { ...options } + ) +}) + +describe.skip('weakMapMemoize simple examples', () => { + const selectorDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + + setFunctionNames({ + selectorDefault, + selectorWeakMap, + selectorAutotrack + }) + + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefault.clearCache() + selectorDefault.resetRecomputations() + selectorDefault.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorDefault.name} recomputations after:`, + selectorDefault.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorWeakMap, + () => { + selectorWeakMap(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorWeakMap.clearCache() + selectorWeakMap.resetRecomputations() + selectorWeakMap.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorWeakMap.name} recomputations after:`, + selectorWeakMap.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorAutotrack, + () => { + selectorAutotrack(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorAutotrack.clearCache() + selectorAutotrack.resetRecomputations() + selectorAutotrack.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorAutotrack.name} recomputations after:`, + selectorAutotrack.recomputations() - 1 + ) + } + } + } + } + ) +}) diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts deleted file mode 100644 index 419ee156..00000000 --- a/test/reselect.bench.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit' -import { bench } from 'vitest' -import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' -import { weakMapMemoize } from '../src/weakMapMemoize' - -const options: NonNullable[2]> = { - iterations: 1_000_000, - time: 100 -} - -describe('bench', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false }, - { id: 2, completed: false }, - { id: 3, completed: false }, - { id: 4, completed: false }, - { id: 5, completed: false }, - { id: 6, completed: false }, - { id: 7, completed: false }, - { id: 8, completed: false }, - { id: 9, completed: false }, - { id: 10, completed: false }, - { id: 11, completed: false }, - { id: 12, completed: false }, - { id: 13, completed: false }, - { id: 14, completed: false }, - { id: 15, completed: false }, - { id: 16, completed: false }, - { id: 17, completed: false }, - { id: 18, completed: false }, - { id: 19, completed: false }, - { id: 20, completed: false }, - { id: 21, completed: false }, - { id: 22, completed: false }, - { id: 23, completed: false }, - { id: 24, completed: false }, - { id: 25, completed: false }, - { id: 26, completed: false }, - { id: 27, completed: false }, - { id: 28, completed: false }, - { id: 29, completed: false }, - { id: 30, completed: false }, - { id: 31, completed: false }, - { id: 32, completed: false }, - { id: 33, completed: false }, - { id: 34, completed: false }, - { id: 35, completed: false }, - { id: 36, completed: false }, - { id: 37, completed: false }, - { id: 38, completed: false }, - { id: 39, completed: false }, - { id: 40, completed: false }, - { id: 41, completed: false }, - { id: 42, completed: false }, - { id: 43, completed: false }, - { id: 44, completed: false }, - { id: 45, completed: false }, - { id: 46, completed: false }, - { id: 47, completed: false }, - { id: 48, completed: false }, - { id: 49, completed: false }, - { id: 50, completed: false }, - { id: 51, completed: false }, - { id: 52, completed: false }, - { id: 53, completed: false }, - { id: 54, completed: false }, - { id: 55, completed: false }, - { id: 56, completed: false }, - { id: 57, completed: false }, - { id: 58, completed: false }, - { id: 59, completed: false }, - { id: 60, completed: false }, - { id: 61, completed: false }, - { id: 62, completed: false }, - { id: 63, completed: false }, - { id: 64, completed: false }, - { id: 65, completed: false }, - { id: 66, completed: false }, - { id: 67, completed: false }, - { id: 68, completed: false }, - { id: 69, completed: false }, - { id: 70, completed: false }, - { id: 71, completed: false }, - { id: 72, completed: false }, - { id: 73, completed: false }, - { id: 74, completed: false }, - { id: 75, completed: false }, - { id: 76, completed: false }, - { id: 77, completed: false }, - { id: 78, completed: false }, - { id: 79, completed: false }, - { id: 80, completed: false }, - { id: 81, completed: false }, - { id: 82, completed: false }, - { id: 83, completed: false }, - { id: 84, completed: false }, - { id: 85, completed: false }, - { id: 86, completed: false }, - { id: 87, completed: false }, - { id: 88, completed: false }, - { id: 89, completed: false }, - { id: 90, completed: false }, - { id: 91, completed: false }, - { id: 92, completed: false }, - { id: 93, completed: false }, - { id: 94, completed: false }, - { id: 95, completed: false }, - { id: 96, completed: false }, - { id: 97, completed: false }, - { id: 98, completed: false }, - { id: 99, completed: false } - ] - } - const selectorDefault = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - const selectorWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) - const selectorArgsWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const parametricSelector = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id] - ) - const parametricSelectorWeakMapArgs = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id], - { - argsMemoize: weakMapMemoize - } - ) - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - options - ) - - bench( - 'selectorAutotrack', - () => { - selectorAutotrack(state) - }, - options - ) - bench( - 'selectorWeakMap', - () => { - selectorWeakMap(state) - }, - options - ) - bench( - 'selectorArgsAutotrack', - () => { - selectorArgsAutotrack(state) - }, - options - ) - bench( - 'selectorArgsWeakMap', - () => { - selectorArgsWeakMap(state) - }, - options - ) - bench( - 'non-memoized selector', - () => { - nonMemoizedSelector(state) - }, - options - ) - bench( - 'parametricSelector', - () => { - parametricSelector(state, 0) - }, - options - ) - bench( - 'parametricSelectorWeakMapArgs', - () => { - parametricSelectorWeakMapArgs(state, 0) - }, - options - ) -}) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index dfb4abfb..59983364 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, @@ -951,7 +941,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') @@ -999,22 +991,121 @@ 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, + // (state: RootState) => ({ ...state.users }), + users => { + console.log('run') + return users.user.details.preferences.notifications.push.frequency + }, + { inputStabilityCheck: 'never' } + ) + let start = performance.now() + for (let i = 0; i < 10_000_000; i++) { + selectorDefault(state) + } + console.log(performance.now() - start) + selectorDefault1(state) + const element2 = store.getState() + selectorDefault2(store.getState(), 0) + const element = store.getState() + store.dispatch(toggleCompleted(0)) + const element1 = store.getState() + console.log(element === element1) + console.log(element.alerts === element1.alerts) + console.log(element.todos[1] === element1.todos[1]) + console.log(element === element2) + console.log(element.alerts === element2.alerts) + selectorDefault2(store.getState(), 0) + selectorDefault2(store.getState(), 0) + selectorDefault2(store.getState(), 0) + start = performance.now() + for (let i = 0; i < 100_000_000; i++) { + selector(state) + } + console.log(selector.memoize.name, performance.now() - start) + start = performance.now() + for (let i = 0; i < 100_000_000; i++) { + selector1(state) + } + console.log(selector1.memoize.name, performance.now() - start) + start = performance.now() + for (let i = 0; i < 100; i++) { + selectorDefault2(store.getState(), 0) + // selectorDefault2({ ...state }, 0) + // selectorDefault2({ users: { user: { id: 0, status: '', details: { preferences: { notifications: { push: { frequency: '' } } } } } } }) + } + console.log( + selectorDefault2.memoize.name, + performance.now() - start, + selectorDefault2.recomputations(), + called + ) + } + ) }) diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 2e34b4d8..78daee56 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 c3f6beb7..eb48e6a0 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,6 +1,13 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' -import type { AnyFunction, Simplify } from '../src/types' +import { test } from 'vitest' +import type { + AnyFunction, + OutputSelector, + Selector, + SelectorArray, + Simplify +} from '../src/types' interface Todo { id: number @@ -16,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 +124,29 @@ const todoState = [ completed: false } ] + +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) + +// for (let i = todoStateLength; i < 200; i++) { +// todoState.push(createTodoItem(i)) +// } const alertState = [ { @@ -103,6 +190,49 @@ const alertState = [ read: false } ] + +// 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', @@ -151,6 +281,13 @@ const alertSlice = createSlice({ alert.read = true } }, + + 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 @@ -165,24 +302,90 @@ 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 = Simplify> -export type RootState = Simplify> +export type RootState = ReturnType export interface LocalTestContext { store: AppStore state: RootState } -export const { markAsRead, addAlert, removeAlert } = alertSlice.actions +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions export const { toggleCompleted, @@ -191,6 +394,8 @@ export const { updateTodo, clearCompleted } = todoSlice.actions + +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions // Since Node 16 does not support `structuredClone` export const deepClone = (object: T): T => @@ -205,3 +410,37 @@ export const setFunctionNames = (funcObject: Record) => { setFunctionName(value, key) ) } + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = >( + selector: Pick +) => { + selector.clearCache() + selector.resetRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = < + S extends Selector & { + recomputations: () => number + dependencyRecomputations: () => number + } +>( + 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 dc51ba69..27e1b870 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 369e121a..b8d91023 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 } diff --git a/tsconfig.json b/tsconfig.json index daa27554..388fab46 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", @@ -15,11 +16,11 @@ "experimentalDecorators": true, "rootDirs": ["./src"], "rootDir": "./src", - "types": ["vitest/globals"], + "types": ["vitest/globals"], "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 00000000..1d58e8ee --- /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/createCurriedSrlector.test-d.ts b/type-tests/createCurriedSrlector.test-d.ts new file mode 100644 index 00000000..6611e22c --- /dev/null +++ b/type-tests/createCurriedSrlector.test-d.ts @@ -0,0 +1,106 @@ +import { createCurriedSelector, createSelector, defaultMemoize } from 'reselect' +import { 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('curried selector', () => { + test('curried selector fields args as array', () => { + const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos[id] + ) + const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos[id] + ) + expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( + parametricSelector.clearCache + ) + expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( + parametricSelector.dependencies + ) + expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( + parametricSelector.lastResult + ) + expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( + parametricSelector.lastResult() + ) + expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( + parametricSelector.memoizedResultFunc + ) + expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( + parametricSelector.recomputations + ) + expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( + parametricSelector.resetRecomputations + ) + expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( + parametricSelector.resultFunc + ) + expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( + parametricSelector.memoizedResultFunc.clearCache + ) + expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( + parametricSelector(state, 0) + ) + }) + + test('curried selector fields separate inline args', () => { + const curriedSelector = createCurriedSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelector = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( + parametricSelector.clearCache + ) + expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( + parametricSelector.dependencies + ) + expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( + parametricSelector.lastResult + ) + expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( + parametricSelector.lastResult() + ) + expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( + parametricSelector.memoizedResultFunc + ) + expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( + parametricSelector.recomputations + ) + expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( + parametricSelector.resetRecomputations + ) + expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( + parametricSelector.resultFunc + ) + expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( + parametricSelector.memoizedResultFunc.clearCache + ) + expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( + parametricSelector(state, 0) + ) + }) +}) diff --git a/type-tests/deepNesting.test-d.ts b/type-tests/deepNesting.test-d.ts new file mode 100644 index 00000000..5a5e69bc --- /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 00000000..7c516c1b --- /dev/null +++ b/type-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "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 649debf4..2093b892 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. diff --git a/typescript_test/test.ts b/typescript_test/test.ts index c6be7e51..379768f6 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -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 } diff --git a/typescript_test/typesTestUtils.ts b/typescript_test/typesTestUtils.ts new file mode 100644 index 00000000..90d0d5b2 --- /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 0ee99ef3..dad9aad5 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: {