Skip to content

Commit

Permalink
Merge pull request #537 from eXamadeus/fix-memoizeOptions-typing
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Nov 4, 2021
2 parents e10ef9c + 835990e commit f2b0815
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 33 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,12 @@ Otherwise, selectors created using `createSelector` only have a cache size of on

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`

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)
Expand Down
26 changes: 14 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type {
OutputSelector,
EqualityFn,
SelectorArray,
SelectorResultArray
SelectorResultArray,
DropFirst
} from './types'

export type {
Expand Down Expand Up @@ -48,19 +49,17 @@ function getDependencies(funcs: unknown[]) {
return dependencies as SelectorArray
}

type DropFirst<T extends unknown[]> = T extends [unknown, ...infer U]
? U
: never

export function createSelectorCreator<
/** Selectors will eventually accept some function to be memoized */
F extends (...args: unknown[]) => unknown,
/** A memoizer such as defaultMemoize that accepts a function + some possible options */
MemoizeFunction extends (func: F, ...options: any[]) => F,
/** The additional options arguments to the memoizer */
MemoizeOptions extends unknown[] = DropFirst<Parameters<MemoizeFunction>>
>(
memoize: MemoizeFunction,
...memoizeOptionsFromArgs: DropFirst<Parameters<MemoizeFunction>>
) {
// (memoize: MemoizeFunction, ...memoizeOptions: MemoizerOptions) {
const createSelector = (...funcs: Function[]) => {
let recomputations = 0
let lastResult: unknown
Expand Down Expand Up @@ -169,9 +168,10 @@ interface CreateSelectorFunction<
Selectors,
Result,
((...args: SelectorResultArray<Selectors>) => Result) &
ReturnType<MemoizeFunction>,
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>,
GetParamsFromSelectors<Selectors>
>
> &
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>

/** Input selectors as separate inline arguments with memoizeOptions passed */
<Selectors extends SelectorArray, Result>(
Expand All @@ -184,9 +184,10 @@ interface CreateSelectorFunction<
Selectors,
Result,
((...args: SelectorResultArray<Selectors>) => Result) &
ReturnType<MemoizeFunction>,
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>,
GetParamsFromSelectors<Selectors>
>
> &
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>

/** Input selectors as a separate array */
<Selectors extends SelectorArray, Result>(
Expand All @@ -197,9 +198,10 @@ interface CreateSelectorFunction<
Selectors,
Result,
((...args: SelectorResultArray<Selectors>) => Result) &
ReturnType<MemoizeFunction>,
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>,
GetParamsFromSelectors<Selectors>
>
> &
Pick<ReturnType<MemoizeFunction>, keyof ReturnType<MemoizeFunction>>
}

export const createSelector =
Expand Down
59 changes: 55 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
/** A standard selector function, which takes three generic type arguments:
* @param State The first value, often a Redux root state object
* @param Result The final result returned by the selector
* @param Params All additional arguments passed into the selector
*/
export type Selector<
S = any,
R = unknown,
P extends never | readonly any[] = any[]
> = [P] extends [never] ? (state: S) => R : (state: S, ...params: P) => R
// The state can be anything
State = any,
// The result will be inferred
Result = unknown,
// There are either 0 params, or N params
Params extends never | readonly any[] = any[]
// If there are 0 params, type the function as just State in, Result out.
// Otherwise, type it as State + Params in, Result out.
> = [Params] extends [never]
? (state: State) => Result
: (state: State, ...params: Params) => Result

/** Selectors generated by Reselect have several additional fields attached: */
interface OutputSelectorFields<Combiner, Result> {
/** The final function passed to `createSelector` */
resultFunc: Combiner
/** The same function, memoized */
memoizedResultFunc: Combiner
/** Returns the last result calculated by the selector */
lastResult: () => Result
/** An array of the input selectors */
dependencies: SelectorArray
/** Counts the number of times the output has been recalculated */
recomputations: () => number
/** Resets the count of recomputations count to 0 */
resetRecomputations: () => number
}

/** Represents the actual selectors generated by `createSelector`.
* The selector is:
* - "a function that takes this state + params and returns a result"
* - plus the attached additional fields
*/
export type OutputSelector<
S extends SelectorArray,
Result,
Expand All @@ -21,19 +45,30 @@ export type OutputSelector<
> = Selector<GetStateFromSelectors<S>, Result, Params> &
OutputSelectorFields<Combiner, Result>

/** A selector that is assumed to have one additional argument, such as
* the props from a React component
*/
export type ParametricSelector<State, Props, Result> = Selector<
State,
Result,
[Props, ...any]
>

/** A generated selector that is assumed to have one additional argument */
export type OutputParametricSelector<State, Props, Result, Combiner> =
ParametricSelector<State, Props, Result> &
OutputSelectorFields<Combiner, Result>

/** An array of input selectors */
export type SelectorArray = ReadonlyArray<Selector>

/** Utility type to extract the State generic from a selector */
type GetStateFromSelector<S> = S extends Selector<infer State> ? State : never

/** Utility type to extract the State generic from multiple selectors at once,
* to help ensure that all selectors correctly share the same State type and
* avoid mismatched input selectors being provided.
*/
export type GetStateFromSelectors<S extends SelectorArray> =
// handle two elements at once so this type works for up to 30 selectors
S extends [infer C1, infer C2, ...infer Other]
Expand All @@ -50,11 +85,17 @@ export type GetStateFromSelectors<S extends SelectorArray> =
? GetStateFromSelector<Elem>
: never

/** Utility type to extract the Params generic from a selector */
export type GetParamsFromSelector<S> = S extends Selector<any, any, infer P>
? P extends []
? never
: P
: never

/** Utility type to extract the Params generic from multiple selectors at once,
* to help ensure that all selectors correctly share the same params and
* avoid mismatched input selectors being provided.
*/
export type GetParamsFromSelectors<S, Found = never> = S extends SelectorArray
? S extends (infer s)[]
? GetParamsFromSelector<s>
Expand All @@ -65,8 +106,12 @@ export type GetParamsFromSelectors<S, Found = never> = S extends SelectorArray
: S
: Found

/** Utility type to extract the return type from a selector */
type SelectorReturnType<S> = S extends Selector ? ReturnType<S> : never

/** Utility type to extract the Result generic from multiple selectors at once,
* for use in calculating the arguments to the "result/combiner" function.
*/
export type SelectorResultArray<
Selectors extends SelectorArray,
Rest extends SelectorArray = Selectors
Expand All @@ -88,4 +133,10 @@ export type SelectorResultArray<
? S[]
: []

/** A standard function returning true if two values are considered equal */
export type EqualityFn = (a: any, b: any) => boolean

/** Utility type to infer the type of "all params of a function except the first", so we can determine what arguments a memoize function accepts */
export type DropFirst<T extends unknown[]> = T extends [unknown, ...infer U]
? U
: never
32 changes: 20 additions & 12 deletions test/test_selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,18 +604,15 @@ describe('defaultMemoize', () => {
})

test('updates the cache key even if resultEqualityCheck is a hit', () => {
const selector = jest.fn(x => x)
const selector = jest.fn(x => x)
const equalityCheck = jest.fn((a, b) => a === b)
const resultEqualityCheck = jest.fn((a, b) => typeof a === typeof b)

const memoizedFn = defaultMemoize(
selector,
{
maxSize: 1,
resultEqualityCheck,
equalityCheck
}
)
const memoizedFn = defaultMemoize(selector, {
maxSize: 1,
resultEqualityCheck,
equalityCheck
})

// initialize the cache
memoizedFn('cache this result')
Expand Down Expand Up @@ -767,11 +764,22 @@ describe('defaultMemoize', () => {

selector.memoizedResultFunc.clearCache()

// Added
selector('a') // ['a']
expect(funcCalls).toBe(4)

// Already in cache
selector('a') // ['a']
expect(funcCalls).toBe(4)

// make sure clearCache is passed to the selector correctly
selector.clearCache()

// Cache was cleared
// Note: the outer arguments wrapper function still has 'c' in its own size-1 cache, so passing
// 'c' here would _not_ recalculate
// Note: the outer arguments wrapper function still has 'a' in its own size-1 cache, so passing
// 'a' here would _not_ recalculate
selector('b') // ['b']
expect(funcCalls).toBe(4)
expect(funcCalls).toBe(5)
})
})

Expand Down
64 changes: 59 additions & 5 deletions typescript_test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
ParametricSelector,
OutputSelector,
SelectorResultArray,
GetParamsFromSelectors
} from '../src/index'
Selector
} from '../src'

import microMemoize from 'micro-memoize'
import memoizeOne from 'memoize-one'
Expand Down Expand Up @@ -536,9 +536,9 @@ function testDefaultMemoize() {
}

function testCreateSelectorCreator() {
const createSelector = createSelectorCreator(defaultMemoize)
const defaultCreateSelector = createSelectorCreator(defaultMemoize)

const selector = createSelector(
const selector = defaultCreateSelector(
(state: { foo: string }) => state.foo,
foo => foo
)
Expand All @@ -547,7 +547,10 @@ function testCreateSelectorCreator() {
// @ts-expect-error
selector({ foo: 'fizz' }, { bar: 42 })

const parametric = createSelector(
// clearCache should exist because of defaultMemoize
selector.clearCache()

const parametric = defaultCreateSelector(
(state: { foo: string }) => state.foo,
(state: { foo: string }, props: { bar: number }) => props.bar,
(foo, bar) => ({ foo, bar })
Expand Down Expand Up @@ -744,6 +747,13 @@ import { GetStateFromSelectors } from '../src/types'
defaultEqualityCheck
)

const select = createMultiMemoizeArgSelector(
(state: { foo: string }) => state.foo,
foo => foo + '!'
)
// @ts-expect-error - not using defaultMemoize, so clearCache shouldn't exist
select.clearCache()

const createMultiMemoizeArgSelector2 = createSelectorCreator(
multiArgMemoize,
42,
Expand Down Expand Up @@ -1129,3 +1139,47 @@ function testInputSelectorWithUndefinedReturn() {
memoizeOptions: { maxSize: 42 }
})
}

function deepNesting() {
type State = { foo: string }
const readOne = (state: State) => state.foo

const selector0 = createSelector(readOne, one => one)
const selector1 = createSelector(selector0, s => s)
const selector2 = createSelector(selector1, s => s)
const selector3 = createSelector(selector2, s => s)
const selector4 = createSelector(selector3, s => s)
const selector5 = createSelector(selector4, s => s)
const selector6 = createSelector(selector5, s => s)
const selector7 = createSelector(selector6, s => s)
const selector8: Selector<State, string> = 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: OutputSelector<
[(state: State) => string],
ReturnType<typeof selector16>,
(s: string) => string,
never
> = 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: Selector<
typeof selector25 extends Selector<infer S> ? S : never,
ReturnType<typeof selector25>
> = createSelector(selector25, s => s)
const selector27 = createSelector(selector26, s => s)
const selector28 = createSelector(selector27, s => s)
const selector29 = createSelector(selector28, s => s)
}

0 comments on commit f2b0815

Please sign in to comment.