Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce pre-typed createSelector via createSelector.withTypes<RootState>() method #673

Merged
merged 17 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/examples/createSelector/annotateResultFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createSelector } from 'reselect'

interface Todo {
id: number
completed: boolean
}

interface Alert {
id: number
read: boolean
}

export interface RootState {
todos: Todo[]
alerts: Alert[]
}

export const createAppSelector = createSelector.withTypes<RootState>()

const selectTodoIds = createAppSelector(
// Type of `state` is set to `RootState`, no need to manually set the type
state => state.todos,
// ❌ Known limitation: Parameter types are not inferred in this scenario
// so you will have to manually annotate them.
// highlight-start
(todos: Todo[]) => todos.map(({ id }) => id)
// highlight-end
)
36 changes: 36 additions & 0 deletions docs/examples/createSelector/createAppSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import microMemoize from 'micro-memoize'
import { shallowEqual } from 'react-redux'
import { createSelectorCreator, lruMemoize } from 'reselect'

export interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

export const createAppSelector = createSelectorCreator({
memoize: lruMemoize,
argsMemoize: microMemoize,
memoizeOptions: {
maxSize: 10,
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual
},
argsMemoizeOptions: {
isEqual: shallowEqual,
maxSize: 10
},
devModeChecks: {
identityFunctionCheck: 'never',
inputStabilityCheck: 'always'
}
}).withTypes<RootState>()

const selectReadAlerts = createAppSelector(
[
// Type of `state` is set to `RootState`, no need to manually set the type
// highlight-start
state => state.alerts
// highlight-end
],
alerts => alerts.filter(({ read }) => read)
)
18 changes: 18 additions & 0 deletions docs/examples/createSelector/withTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createSelector } from 'reselect'

export interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}

export const createAppSelector = createSelector.withTypes<RootState>()

const selectTodoIds = createAppSelector(
[
// Type of `state` is set to `RootState`, no need to manually set the type
// highlight-start
state => state.todos
// highlight-end
],
todos => todos.map(({ id }) => id)
)
64 changes: 56 additions & 8 deletions src/createSelectorCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import {
*
* @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`).
* @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). If none is explicitly provided, `weakMapMemoize` will be used.
* @template StateType - The type of state that the selectors created with this selector creator will operate on.
*
* @public
*/
export interface CreateSelectorFunction<
MemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize
ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize,
StateType = any
> {
/**
* Creates a memoized selector function.
Expand All @@ -47,9 +49,9 @@ export interface CreateSelectorFunction<
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector}
* @see {@link https://reselect.js.org/api/createselector `createSelector`}
*/
<InputSelectors extends SelectorArray, Result>(
<InputSelectors extends SelectorArray<StateType>, Result>(
...createSelectorArgs: [
...inputSelectors: InputSelectors,
combiner: Combiner<InputSelectors, Result>
Expand All @@ -73,10 +75,10 @@ export interface CreateSelectorFunction<
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector}
* @see {@link https://reselect.js.org/api/createselector `createSelector`}
*/
<
InputSelectors extends SelectorArray,
InputSelectors extends SelectorArray<StateType>,
Result,
OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
Expand Down Expand Up @@ -114,10 +116,10 @@ export interface CreateSelectorFunction<
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector}
* @see {@link https://reselect.js.org/api/createselector `createSelector`}
*/
<
InputSelectors extends SelectorArray,
InputSelectors extends SelectorArray<StateType>,
Result,
OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
Expand All @@ -139,6 +141,46 @@ export interface CreateSelectorFunction<
OverrideArgsMemoizeFunction
> &
InterruptRecursion

/**
* Creates a "pre-typed" version of {@linkcode createSelector createSelector}
* where the `state` type is predefined.
*
* This allows you to set the `state` type once, eliminating the need to
* specify it with every {@linkcode createSelector createSelector} call.
*
* @returns A pre-typed `createSelector` with the state type already defined.
*
* @example
* ```ts
* import { createSelector } from 'reselect'
*
* export interface RootState {
* todos: { id: number; completed: boolean }[]
* alerts: { id: number; read: boolean }[]
* }
*
* export const createAppSelector = createSelector.withTypes<RootState>()
*
* const selectTodoIds = createAppSelector(
* [
* // Type of `state` is set to `RootState`, no need to manually set the type
* state => state.todos
* ],
* todos => todos.map(({ id }) => id)
* )
* ```
* @template OverrideStateType - The specific type of state used by all selectors created with this selector creator.
*
* @see {@link https://reselect.js.org/api/createselector#defining-a-pre-typed-createselector `createSelector.withTypes`}
*
* @since 5.0.2
*/
withTypes<OverrideStateType extends StateType>(): CreateSelectorFunction<
MemoizeFunction,
ArgsMemoizeFunction,
OverrideStateType
>
}

/**
Expand Down Expand Up @@ -226,7 +268,8 @@ export function createSelectorCreator<MemoizeFunction extends UnknownMemoizer>(
): CreateSelectorFunction<MemoizeFunction>

/**
* Creates a selector creator function with the specified memoization function and options for customizing memoization behavior.
* Creates a selector creator function with the specified memoization
* function and options for customizing memoization behavior.
*
* @param memoizeOrOptions - Either A `memoize` function or an `options` object containing the `memoize` function.
* @param memoizeOptionsFromArgs - Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards.
Expand Down Expand Up @@ -426,6 +469,11 @@ export function createSelectorCreator<
OverrideArgsMemoizeFunction
>
}

Object.assign(createSelector, {
withTypes: () => createSelector
})

return createSelector as CreateSelectorFunction<
MemoizeFunction,
ArgsMemoizeFunction
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type Selector<
*
* @public
*/
export type SelectorArray = readonly Selector[]
export type SelectorArray<State = any> = readonly Selector<State>[]

/**
* Extracts an array of all return types from all input selectors.
Expand Down
23 changes: 23 additions & 0 deletions test/createSelector.withTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createSelector } from 'reselect'
import type { RootState } from './testUtils'
import { localTest } from './testUtils'

describe(createSelector.withTypes, () => {
const createTypedSelector = createSelector.withTypes<RootState>()

localTest('should return createSelector', ({ state }) => {
expect(createTypedSelector.withTypes).to.be.a('function')

expect(createTypedSelector.withTypes().withTypes).to.be.a('function')

expect(createTypedSelector).toBe(createSelector)

const selectTodoIds = createTypedSelector([state => state.todos], todos =>
todos.map(({ id }) => id)
)

expect(selectTodoIds).toBeMemoizedSelector()

expect(selectTodoIds(state)).to.be.an('array').that.is.not.empty
})
})
10 changes: 10 additions & 0 deletions test/customMatchers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Assertion, AsymmetricMatchersContaining } from 'vitest'

interface CustomMatchers<R = unknown> {
toBeMemoizedSelector(): R
}

declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
37 changes: 2 additions & 35 deletions test/reselect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { RootState } from './testUtils'
import {
addTodo,
deepClone,
isMemoizedSelector,
localTest,
setEnvToProd,
toggleCompleted
Expand Down Expand Up @@ -479,43 +480,9 @@ describe('argsMemoize and memoize', () => {
const hasUndefinedValues = (object: object) => {
return Object.values(object).some(e => e == null)
}
const isMemoizedSelector = (selector: object) => {
return (
typeof selector === 'function' &&
'resultFunc' in selector &&
'memoizedResultFunc' in selector &&
'lastResult' in selector &&
'dependencies' in selector &&
'recomputations' in selector &&
'dependencyRecomputations' in selector &&
'resetRecomputations' in selector &&
'resetDependencyRecomputations' in selector &&
'memoize' in selector &&
'argsMemoize' in selector &&
typeof selector.resultFunc === 'function' &&
typeof selector.memoizedResultFunc === 'function' &&
typeof selector.lastResult === 'function' &&
Array.isArray(selector.dependencies) &&
typeof selector.recomputations === 'function' &&
typeof selector.dependencyRecomputations === 'function' &&
typeof selector.resetRecomputations === 'function' &&
typeof selector.resetDependencyRecomputations === 'function' &&
typeof selector.memoize === 'function' &&
typeof selector.argsMemoize === 'function' &&
selector.dependencies.length >= 1 &&
selector.dependencies.every(
(dependency): dependency is Function =>
typeof dependency === 'function'
) &&
!selector.lastResult.length &&
!selector.recomputations.length &&
!selector.resetRecomputations.length &&
typeof selector.recomputations() === 'number'
)
}
const isArrayOfFunctions = (array: any[]) =>
array.every(e => typeof e === 'function')
expect(selectorDefault).toSatisfy(isMemoizedSelector)
expect(selectorDefault).toBeMemoizedSelector()
expect(selectorDefault)
.to.be.a('function')
.that.has.all.keys(allFields)
Expand Down
12 changes: 12 additions & 0 deletions test/setup.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { isMemoizedSelector } from './testUtils'

expect.extend({
toBeMemoizedSelector(received) {
const { isNot } = this

return {
pass: isMemoizedSelector(received),
message: () => `${received} is${isNot ? '' : ' not'} a memoized selector`
}
}
})
35 changes: 35 additions & 0 deletions test/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,38 @@ export const setEnvToProd = () => {
process.env.NODE_ENV = originalEnv
}
}

export const isMemoizedSelector = (selector: object) => {
return (
typeof selector === 'function' &&
'resultFunc' in selector &&
'memoizedResultFunc' in selector &&
'lastResult' in selector &&
'dependencies' in selector &&
'recomputations' in selector &&
'dependencyRecomputations' in selector &&
'resetRecomputations' in selector &&
'resetDependencyRecomputations' in selector &&
'memoize' in selector &&
'argsMemoize' in selector &&
typeof selector.resultFunc === 'function' &&
typeof selector.memoizedResultFunc === 'function' &&
typeof selector.lastResult === 'function' &&
Array.isArray(selector.dependencies) &&
typeof selector.recomputations === 'function' &&
typeof selector.dependencyRecomputations === 'function' &&
typeof selector.resetRecomputations === 'function' &&
typeof selector.resetDependencyRecomputations === 'function' &&
typeof selector.memoize === 'function' &&
typeof selector.argsMemoize === 'function' &&
selector.dependencies.length >= 1 &&
selector.dependencies.every(
(dependency): dependency is Function => typeof dependency === 'function'
) &&
!selector.lastResult.length &&
!selector.recomputations.length &&
!selector.resetRecomputations.length &&
typeof selector.recomputations() === 'number' &&
typeof selector.dependencyRecomputations() === 'number'
)
}
6 changes: 2 additions & 4 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"target": "ESNext",
"jsx": "react",
"baseUrl": ".",
"rootDir": ".",
"rootDir": "../",
"skipLibCheck": true,
"noImplicitReturns": false,
"noUnusedLocals": false,
Expand All @@ -21,7 +21,5 @@
"@internal/*": ["../src/*"]
}
},
"include": [
"**/*.ts*",
]
"include": ["**/*.ts*"]
}
Loading
Loading