reselect-kit 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +307 -36
  2. package/dist/cjs/reselectKit.development.cjs +256 -95
  3. package/dist/cjs/reselectKit.development.cjs.map +1 -1
  4. package/dist/cjs/reselectKit.production.min.cjs +1 -1
  5. package/dist/cjs/reselectKit.production.min.cjs.map +1 -1
  6. package/dist/reselectKit.d.ts +201 -92
  7. package/dist/reselectKit.mjs +257 -97
  8. package/dist/reselectKit.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/createAdaptedSelector.test.ts +3 -4
  11. package/src/__tests__/createBoundSelector.test.ts +92 -5
  12. package/src/__tests__/createChainSelector.bench.ts +117 -0
  13. package/src/__tests__/createChainSelector.test.ts +162 -6
  14. package/src/__tests__/createPathSelector.test.ts +14 -0
  15. package/src/__tests__/createSequenceSelector.test.ts +10 -14
  16. package/src/_helpers/__tests__/generateSelectorKey.test.ts +24 -0
  17. package/src/_helpers/__tests__/getSelectorName.test.ts +30 -0
  18. package/src/_helpers/unfreezeProps.ts +14 -0
  19. package/src/_hooks/__tests__/useSelector.bench.ts +133 -0
  20. package/src/_hooks/__tests__/useSelector.test.ts +69 -0
  21. package/src/_hooks/__tests__/useSelectors.bench.ts +160 -0
  22. package/src/_hooks/__tests__/useSelectors.test.ts +219 -0
  23. package/src/_hooks/selectorOptions.ts +11 -0
  24. package/src/_hooks/useSelector.ts +68 -26
  25. package/src/_hooks/useSelectors.ts +162 -0
  26. package/src/cache/__tests__/intervalMapCache.test.ts +11 -0
  27. package/src/createAdaptedSelector.ts +9 -5
  28. package/src/createBoundSelector.ts +92 -84
  29. package/src/createCachedSequenceSelector.ts +12 -8
  30. package/src/createCachedStructuredSelector.ts +11 -20
  31. package/src/createChainSelector.ts +134 -89
  32. package/src/createPathSelector.ts +71 -104
  33. package/src/createPropSelector.ts +9 -8
  34. package/src/createSequenceSelector.ts +14 -9
  35. package/src/index.ts +2 -0
  36. package/src/keys/createKeySelectorComposer.ts +12 -23
  37. package/src/types.ts +115 -7
@@ -1,5 +1,4 @@
1
- import { Selector } from '@veksa/reselect';
2
- import { CachedSelector, NamedParametricSelector } from './types';
1
+ import { CachedSelector, NamedSelector } from './types';
3
2
  import { withDebugName } from './debug/withDebugName';
4
3
  import { getSelectorName } from './_helpers/getSelectorName';
5
4
  import { isCachedSelector } from './_helpers/isCachedSelector';
@@ -25,15 +24,20 @@ const generateMappingName = <P1 extends object, P2>(mapping: (props: P2) => P1)
25
24
 
26
25
  const innerCreateAdaptedSelector =
27
26
  <S, P1, P2, R>(
28
- baseSelector: Selector<S, R, [P1]>,
27
+ baseSelector: (state: S, props: P1) => R,
29
28
  mapping: (props: P2) => P1,
30
- ): NamedParametricSelector<S, P2, R> =>
29
+ ): NamedSelector<S, R, [P2]> =>
31
30
  (state: S, props: P2) => {
32
31
  return baseSelector(state, mapping(props));
33
32
  };
34
33
 
34
+ /**
35
+ * Rewrites the props a selector reads. Only the props argument changes: the
36
+ * adapted selector takes `[P2]` where the base took `[P1]`, which is the same
37
+ * argument tuple every other creator in the library carries.
38
+ */
35
39
  export const createAdaptedSelector = <S, P1 extends object, P2, R>(
36
- baseSelector: Selector<S, R, [P1]>,
40
+ baseSelector: (state: S, props: P1) => R,
37
41
  mapping: (props: P2) => P1,
38
42
  ) => {
39
43
  const adaptedSelector = innerCreateAdaptedSelector(baseSelector, mapping);
@@ -1,5 +1,4 @@
1
- import { Selector } from '@veksa/reselect';
2
- import { CachedSelector, NamedParametricSelector, NamedSelector } from './types';
1
+ import { CachedSelector, NamedSelector } from './types';
3
2
  import { isComposedKeySelector, KeySelectorComposer } from './keys/createKeySelectorComposer';
4
3
  import { isPropSelector } from './createPropSelector';
5
4
  import { excludeDefaultSelectors } from './keys/createKeySelectorCreator';
@@ -28,47 +27,119 @@ const generateBindingName = <P extends object>(binding: P) => {
28
27
  };
29
28
 
30
29
  /**
31
- * The special type to prevent binding of non optional props on optional values
30
+ * The bound selector: what the binding did not supply is what is left to pass.
31
+ * A binding covering every prop leaves an empty argument tuple, which is what a
32
+ * plain selector is in the unified model — so one type covers both cases.
33
+ *
34
+ * The outer condition is the guard against binding a prop the base selector
35
+ * declares as optional with a value that is not.
32
36
  */
33
37
  export type BoundSelector<S, P2, P1 extends Partial<P2>, R> =
34
38
  P2 extends Pick<P1, keyof P2>
35
- ? Exclude<keyof P1, keyof P2> extends never
36
- ? NamedSelector<S, R>
37
- : NamedParametricSelector<S, Omit<P1, keyof P2>, R>
39
+ ? NamedSelector<S, R, Exclude<keyof P1, keyof P2> extends never ? [] : [Omit<P1, keyof P2>]>
38
40
  : never;
39
41
 
40
42
  export type BoundSelectorOptions<S, P2, P1 extends Partial<P2>, R> = {
41
43
  bindingStrategy?: (
42
- baseSelector: Selector<S, R, [P1]>,
44
+ baseSelector: (state: S, props: P1) => R,
43
45
  binding: P2,
44
- ) => Selector<S, R, [Omit<P1, keyof P2>]>;
46
+ ) => (state: S, props: Omit<P1, keyof P2>) => R;
45
47
  keySelectorComposer?: KeySelectorComposer;
46
48
  };
47
49
 
48
- const innerCreateBoundSelector = <S, P2, P1 extends Partial<P2>, R>(
49
- baseSelector: Selector<S, R, [P1]>,
50
+ /**
51
+ * The default binding strategy: applies the binding to the props the caller
52
+ * passed, hands them to the base selector, and puts them back as they were.
53
+ *
54
+ * Almost every binding is a single key — `bound(selector, { positionId })` —
55
+ * and specialising it removes the two allocations the generic path costs per
56
+ * call: the `{ result, rollback }` object and the `rollback` closure. On a
57
+ * graph with a bound selector per row those are the calls that dominate.
58
+ *
59
+ * The `delete` in the restore stays deliberately. Leaving the key behind as
60
+ * `undefined` would be cheaper — it avoids putting the caller's props object
61
+ * into dictionary mode — but it also changes the object's shape, and
62
+ * `useSelector` memoizes props shallowly: a props object that has grown an
63
+ * extra key never again compares equal to a freshly built one, so the
64
+ * memoization the binding exists to protect would be defeated.
65
+ */
66
+ const applyBinding = <S, P2, P1 extends Partial<P2>, R>(
67
+ baseSelector: (state: S, props: P1) => R,
50
68
  binding: P2,
51
69
  ) => {
52
- const boundSelector = (state: S, props: Omit<P1, keyof P2> | void) =>
53
- baseSelector(state, {
54
- ...props,
55
- ...binding,
56
- } as unknown as P1);
70
+ const source = binding as unknown as Record<string, unknown>;
71
+ const keys = Object.keys(source);
72
+
73
+ if (keys.length === 1) {
74
+ const key = keys[0];
75
+ const value = source[key];
76
+
77
+ return (state: S, props: Omit<P1, keyof P2> | void) => {
78
+ const target = props as unknown as Record<string, unknown>;
79
+
80
+ // Nothing to merge into when the selector is read without props, which is
81
+ // what a fully bound selector is called like.
82
+ if (typeof target !== 'object' || target === null) {
83
+ return baseSelector(state, source as unknown as P1);
84
+ }
85
+
86
+ const previous = target[key];
87
+
88
+ // A re-entrant call that finds the binding already applied has nothing to
89
+ // save and nothing to restore.
90
+ if (previous === value) {
91
+ return baseSelector(state, props as unknown as P1);
92
+ }
57
93
 
58
- return boundSelector as BoundSelector<S, P2, P1, R>;
94
+ const had = Object.prototype.hasOwnProperty.call(target, key);
95
+ target[key] = value;
96
+
97
+ try {
98
+ return baseSelector(state, props as unknown as P1);
99
+ } finally {
100
+ if (had) {
101
+ target[key] = previous;
102
+ } else {
103
+ delete target[key];
104
+ }
105
+ }
106
+ };
107
+ }
108
+
109
+ return (state: S, props: Omit<P1, keyof P2> | void) => {
110
+ // `temporaryAssign` falls back to the binding itself when there are no props
111
+ // to merge into, which is the read of a fully bound selector.
112
+ const { result: nextProps, rollback } = temporaryAssign(
113
+ props as unknown as Record<string, unknown>,
114
+ source,
115
+ );
116
+
117
+ // Restored through `finally` for the same reason the single-key path does
118
+ // it: the props object belongs to the caller, and a selector that throws
119
+ // must not leave the binding written into it.
120
+ try {
121
+ return baseSelector(state, nextProps as unknown as P1);
122
+ } finally {
123
+ rollback();
124
+ }
125
+ };
59
126
  };
60
127
 
61
- const createBoundInnerSelector = <S, P2 extends object, P1 extends Partial<P2>, R, OR extends R>(
62
- baseSelector: Selector<S, R, [P1]>,
128
+ export const createBoundSelector = <S, P2 extends object, P1 extends Partial<P2>, R, OR extends R>(
129
+ baseSelector: (state: S, props: P1) => R,
63
130
  binding: P2,
64
131
  options: BoundSelectorOptions<S, P2, P1, OR> = {},
65
132
  ): BoundSelector<S, P2, P1, R> => {
66
- const bindingStrategy =
67
- (options.bindingStrategy as typeof innerCreateBoundSelector) ?? innerCreateBoundSelector;
133
+ const bindingStrategy = (options.bindingStrategy as typeof applyBinding) ?? applyBinding;
68
134
 
69
135
  const keySelectorComposer = options.keySelectorComposer ?? stringComposeKeySelectors;
70
136
 
71
- const boundSelector = bindingStrategy(baseSelector, binding);
137
+ const boundSelector = bindingStrategy(baseSelector, binding) as unknown as BoundSelector<
138
+ S,
139
+ P2,
140
+ P1,
141
+ R
142
+ >;
72
143
 
73
144
  Object.assign(boundSelector, baseSelector);
74
145
  boundSelector.dependencies = [baseSelector];
@@ -132,66 +203,3 @@ const createBoundInnerSelector = <S, P2 extends object, P1 extends Partial<P2>,
132
203
 
133
204
  return boundSelector;
134
205
  };
135
-
136
- export const createBoundSelector: typeof createBoundInnerSelector = (baseSelector, binding) => {
137
- return createBoundInnerSelector(baseSelector, binding, {
138
- bindingStrategy: (selector, bindingProps) => {
139
- const source = bindingProps as unknown as Record<string, unknown>;
140
- const keys = Object.keys(source);
141
-
142
- /**
143
- * Almost every binding is a single key — `bound(selector, { positionId })` —
144
- * and specialising it removes the two allocations the generic path costs per
145
- * call: the `{ result, rollback }` object and the `rollback` closure. On a
146
- * graph with a bound selector per row those are the calls that dominate.
147
- *
148
- * The `delete` in the restore stays deliberately. Leaving the key behind as
149
- * `undefined` would be cheaper — it avoids putting the caller's props object
150
- * into dictionary mode — but it also changes the object's shape, and
151
- * `useSelector` memoizes props shallowly: a props object that has grown an
152
- * extra key never again compares equal to a freshly built one, so the
153
- * memoization the binding exists to protect would be defeated.
154
- */
155
- if (keys.length === 1) {
156
- const key = keys[0];
157
- const value = source[key];
158
-
159
- return (state, props) => {
160
- const target = props as unknown as Record<string, unknown>;
161
-
162
- if (typeof target !== 'object' || target === null) {
163
- return selector(state, source as Parameters<typeof baseSelector>[1]);
164
- }
165
-
166
- const previous = target[key];
167
-
168
- // A re-entrant call that finds the binding already applied has nothing to
169
- // save and nothing to restore.
170
- if (previous === value) {
171
- return selector(state, props as Parameters<typeof baseSelector>[1]);
172
- }
173
-
174
- const had = Object.prototype.hasOwnProperty.call(target, key);
175
- target[key] = value;
176
-
177
- try {
178
- return selector(state, props as Parameters<typeof baseSelector>[1]);
179
- } finally {
180
- if (had) {
181
- target[key] = previous;
182
- } else {
183
- delete target[key];
184
- }
185
- }
186
- };
187
- }
188
-
189
- return (state, props) => {
190
- const { result: nextProps, rollback } = temporaryAssign(props, source);
191
- const result = selector(state, nextProps as Parameters<typeof baseSelector>[1]);
192
- rollback();
193
- return result;
194
- };
195
- },
196
- });
197
- };
@@ -1,14 +1,18 @@
1
- import { createSelector, Selector } from '@veksa/reselect';
1
+ import { createSelector, Selector, SelectorResultArray } from '@veksa/reselect';
2
2
  import { createCachedSelector, PolymorphicCachedOptions } from '@veksa/re-reselect';
3
3
  import { createSequenceSelector } from './createSequenceSelector';
4
+ import { AnySelector, MergedParams, MergedState } from './types';
4
5
 
5
- export function createCachedSequenceSelector<S, R>(
6
- selectors: Selector<S, R>[],
7
- ): (options: PolymorphicCachedOptions<Selector<S, R>[], R[]>) => Selector<S, R[]>;
8
-
9
- export function createCachedSequenceSelector<S, P, R>(
10
- selectors: Selector<S, R, [P]>[],
11
- ): (options: PolymorphicCachedOptions<Selector<S, R, [P]>[], R[]>) => Selector<S, R[], [P]>;
6
+ /**
7
+ * The cached counterpart of {@link createSequenceSelector}. The inferred tuple
8
+ * reaches `PolymorphicCachedOptions` as-is, so re-reselect types the caller's
9
+ * `keySelector` with the exact arguments of the group.
10
+ */
11
+ export function createCachedSequenceSelector<Selectors extends readonly AnySelector[]>(
12
+ selectors: [...Selectors],
13
+ ): (
14
+ options: PolymorphicCachedOptions<Selectors, SelectorResultArray<Selectors>>,
15
+ ) => Selector<MergedState<Selectors>, SelectorResultArray<Selectors>, MergedParams<Selectors>>;
12
16
 
13
17
  export function createCachedSequenceSelector(selectors: any): any {
14
18
  return createSequenceSelector(
@@ -1,28 +1,19 @@
1
1
  import { createSelector, Selector, SelectorResultsMap, SelectorsObject } from '@veksa/reselect';
2
2
  import { createCachedSelector, PolymorphicCachedOptions } from '@veksa/re-reselect';
3
3
  import { createStructuredSelector } from './createStructuredSelector';
4
- import { UnionToIntersection } from './types';
4
+ import { MergedParams, MergedState } from './types';
5
5
 
6
- /** Intersection of the `state` argument of every selector in the object. */
7
- type StructuredState<M extends SelectorsObject> = UnionToIntersection<
8
- { [K in keyof M]: Parameters<M[K]>[0] }[keyof M]
9
- >;
10
-
11
- /** Union of the `props` argument (position 1) of every parametric selector. */
12
- type StructuredPropsUnion<M extends SelectorsObject> = {
13
- [K in keyof M]: Parameters<M[K]> extends [any, infer Props, ...any[]] ? Props : never;
14
- }[keyof M];
15
-
16
- /** The extra-arguments tuple: `[Props]` when any selector is parametric, else `[]`. */
17
- type StructuredParams<M extends SelectorsObject> = [StructuredPropsUnion<M>] extends [never]
18
- ? []
19
- : [UnionToIntersection<StructuredPropsUnion<M>>];
20
-
21
- /** The input selectors as reselect sees them, used to type the cached options. */
6
+ /**
7
+ * The input selectors as re-reselect reads them when typing the caller's
8
+ * `keySelector`: it takes `state` and the extra arguments off the group, and
9
+ * both are already merged here. Putting the selectors back in key order would
10
+ * type the arguments no more precisely and would cost a type instantiation per
11
+ * key, capping the batch at roughly fifty fields.
12
+ */
22
13
  type StructuredInputs<M extends SelectorsObject> = Selector<
23
- StructuredState<M>,
14
+ MergedState<M>,
24
15
  unknown,
25
- StructuredParams<M>
16
+ MergedParams<M>
26
17
  >[];
27
18
 
28
19
  /**
@@ -34,7 +25,7 @@ export function createCachedStructuredSelector<M extends SelectorsObject>(
34
25
  selectors: M,
35
26
  ): (
36
27
  options: PolymorphicCachedOptions<StructuredInputs<M>, SelectorResultsMap<M>>,
37
- ) => Selector<StructuredState<M>, SelectorResultsMap<M>, StructuredParams<M>>;
28
+ ) => Selector<MergedState<M>, SelectorResultsMap<M>, MergedParams<M>>;
38
29
 
39
30
  export function createCachedStructuredSelector(selectors: any): any {
40
31
  return createStructuredSelector(
@@ -1,30 +1,31 @@
1
- import { createSelectorCreator, Selector } from '@veksa/reselect';
1
+ import { createSelector, createSelectorCreator, Selector } from '@veksa/reselect';
2
2
  import { createCachedSelector, ICacheObject, KeySelector } from '@veksa/re-reselect';
3
3
  import { KeySelectorComposer } from './keys/createKeySelectorComposer';
4
4
  import { isCachedSelector } from './_helpers/isCachedSelector';
5
5
  import { defaultKeySelector } from './keys/defaultKeySelector';
6
6
  import { stringComposeKeySelectors } from './keys/stringComposeKeySelectors';
7
7
  import { createKeySelectorCreator } from './keys/createKeySelectorCreator';
8
+ import { MergeParams } from './types';
8
9
  import { runInDebug, withDebugName } from './debug/withDebugName';
9
10
  import { defineDynamicSelectorName } from './_helpers/defineDynamicSelectorName';
10
11
  import { getSelectorName } from './_helpers/getSelectorName';
11
12
  import { generateSelectorKey } from './_helpers/generateSelectorKey';
12
13
  import { stringifyFunction } from './_helpers/stringifyFunction';
13
14
 
14
- export type SelectorChain<R1, S1, R2> = (result: R1) => Selector<S1, R2>;
15
-
16
- export type ParametricSelectorChain<R1, S1, P1, R2> = (result: R1) => Selector<S1, R2, [P1]>;
15
+ /**
16
+ * A chain step: given the previous level's result, it returns the selector the
17
+ * next level reads. `Params` is empty for a step that only needs `state`, so a
18
+ * single type covers what used to be a plain and a parametric variant.
19
+ */
20
+ export type SelectorChain<R1, S1, R2, Params extends readonly any[] = []> = (
21
+ result: R1,
22
+ ) => Selector<S1, R2, Params>;
17
23
 
18
24
  export type SelectorChainHierarchy<
19
- C extends SelectorChain<any, any, any>,
25
+ C extends SelectorChain<any, any, any, any>,
20
26
  H extends SelectorChainHierarchy<any, any>,
21
27
  > = C & { parentChain?: H };
22
28
 
23
- export type ParametricSelectorChainHierarchy<
24
- C extends ParametricSelectorChain<any, any, any, any>,
25
- H extends ParametricSelectorChainHierarchy<any, any>,
26
- > = C & { parentChain?: H };
27
-
28
29
  export type CreateSelectorOptions<SelectorCreator extends typeof createSelectorCreator> = {
29
30
  selectorCreator: ReturnType<SelectorCreator>;
30
31
  cacheObject: ICacheObject;
@@ -37,52 +38,46 @@ export type ChainSelectorOptions<
37
38
  keySelectorComposer?: KeySelectorComposer;
38
39
  };
39
40
 
40
- interface ChainSelectorResult<S1, R1> {
41
- chain: <S2, R2 = any>(
42
- fn: SelectorChain<R1, S2, R2>,
41
+ /**
42
+ * Every level of the chain carries the arguments its selector needs. A level
43
+ * that only reads `state` leaves `Params` empty, and one that reads props adds
44
+ * them — so a step introducing props makes them required for the whole built
45
+ * selector, whether or not the base selector had any.
46
+ *
47
+ * `Params` defaults to empty on the entry point, so that
48
+ * `createChainSelector<State, Result>(selector)` keeps working for callers that
49
+ * spell the type arguments out.
50
+ */
51
+ interface ChainSelectorResult<S1, R1, Params extends readonly any[]> {
52
+ chain: <S2, R2, Params2 extends readonly any[]>(
53
+ fn: (result: R1) => (state: S2, ...params: Params2) => R2,
43
54
  chainOptions?: ChainSelectorOptions,
44
- ) => ChainSelectorResult<S1 & S2, R2>;
55
+ ) => ChainSelectorResult<S1 & S2, R2, MergeParams<Params, Params2>>;
45
56
  map: <R2>(
46
57
  fn: (result: R1) => R2,
47
58
  mapOptions?: ChainSelectorOptions,
48
- ) => ChainSelectorResult<S1, R2>;
49
- build: () => Selector<S1, R1, []> & {
59
+ ) => ChainSelectorResult<S1, R2, Params>;
60
+ build: () => Selector<S1, R1, Params> & {
50
61
  chainHierarchy: SelectorChainHierarchy<any, any> | undefined;
51
62
  };
52
63
  }
53
64
 
54
- interface ParametricChainSelectorResult<S1, P1, R1> {
55
- chain: <S2, P2 = unknown, R2 = any>(
56
- fn: ParametricSelectorChain<R1, S2, P2, R2>,
57
- chainOptions?: ChainSelectorOptions,
58
- ) => ParametricChainSelectorResult<S1 & S2, P1 & P2, R2>;
59
- map: <R2>(
60
- fn: (result: R1) => R2,
61
- mapOptions?: ChainSelectorOptions,
62
- ) => ParametricChainSelectorResult<S1, P1, R2>;
63
- build: () => Selector<S1, R1, [P1]> & {
64
- chainHierarchy: ParametricSelectorChainHierarchy<any, any> | undefined;
65
- };
66
- }
67
-
68
- export function createChainSelector<S1, R1>(
69
- selector: Selector<S1, R1>,
65
+ export function createChainSelector<S1, R1, Params extends readonly any[] = []>(
66
+ selector: (state: S1, ...params: Params) => R1,
70
67
  options?: ChainSelectorOptions,
71
68
  prevChain?: SelectorChainHierarchy<any, any>,
72
- ): ChainSelectorResult<S1, R1>;
69
+ ): ChainSelectorResult<S1, R1, Params>;
73
70
 
74
- export function createChainSelector<S1, P1, R1>(
75
- selector: Selector<S1, R1, [P1]>,
76
- options?: ChainSelectorOptions,
77
- prevChain?: ParametricSelectorChainHierarchy<any, any>,
78
- ): ParametricChainSelectorResult<S1, P1, R1>;
79
-
80
- export function createChainSelector<S1, P1, R1>(
71
+ export function createChainSelector(
81
72
  selector: any,
82
73
  options?: ChainSelectorOptions,
83
74
  prevChain?: any,
84
75
  ): any {
85
- const chain = <S2, P2, R2>(fn: any, chainOptions?: ChainSelectorOptions) => {
76
+ const createLevel = (
77
+ fn: any,
78
+ chainOptions: ChainSelectorOptions | undefined,
79
+ isDerivedKeyless: boolean,
80
+ ) => {
86
81
  const combinedOptions = {
87
82
  ...options,
88
83
  ...chainOptions,
@@ -95,13 +90,29 @@ export function createChainSelector<S1, P1, R1>(
95
90
 
96
91
  const keySelectorCreator = createKeySelectorCreator(keySelectorComposer);
97
92
 
98
- const higherOrderSelector = createCachedSelector(
99
- [selector],
100
- fn,
101
- )({
102
- ...createSelectorOptions(),
103
- keySelector,
104
- });
93
+ /**
94
+ * Nothing below this level is keyed, so the cache would hold a single entry
95
+ * for the lifetime of the chain and every call would still pay for the key
96
+ * computation and the lookup that guard it. A plain memoized selector is the
97
+ * same mapping — re-reselect under a constant key is one `createSelector`
98
+ * instance — without the layer.
99
+ */
100
+ const isKeyless = keySelector === defaultKeySelector;
101
+
102
+ const selectorOptions = createSelectorOptions() as Partial<CreateSelectorOptions<any>>;
103
+
104
+ const higherOrderSelector = isKeyless
105
+ ? ((selectorOptions.selectorCreator ?? createSelector) as typeof createSelector)(
106
+ [selector],
107
+ fn,
108
+ )
109
+ : createCachedSelector(
110
+ [selector],
111
+ fn,
112
+ )({
113
+ ...selectorOptions,
114
+ keySelector,
115
+ });
105
116
 
106
117
  withDebugName(higherOrderSelector, () => {
107
118
  const baseName = getSelectorName(selector);
@@ -109,13 +120,13 @@ export function createChainSelector<S1, P1, R1>(
109
120
  return `higher order for ${baseName} (${stringifyFunction(fn)})`;
110
121
  });
111
122
 
112
- type CombineSelector = Selector<S1 & S2, R1 & R2, [P1 & P2]> & {
123
+ type CombineSelector = Selector<any, any, [any]> & {
113
124
  dependencies: any;
114
- cache: ICacheObject;
115
- keySelector?: KeySelector<S1 & S2>;
125
+ cache?: ICacheObject;
126
+ keySelector?: KeySelector<any>;
116
127
  };
117
128
 
118
- const combinedSelector: CombineSelector = (state: S1 & S2, props: P1 & P2): R1 & R2 => {
129
+ const combinedSelector: CombineSelector = (state: any, props: any): any => {
119
130
  const derivedSelector: any = (higherOrderSelector as any)(state, props);
120
131
 
121
132
  runInDebug(() => {
@@ -147,40 +158,74 @@ export function createChainSelector<S1, P1, R1>(
147
158
  };
148
159
 
149
160
  combinedSelector.dependencies = [higherOrderSelector];
150
- combinedSelector.cache = higherOrderSelector.cache;
161
+
162
+ if (!isKeyless) {
163
+ combinedSelector.cache = (higherOrderSelector as unknown as { cache: ICacheObject }).cache;
164
+ }
151
165
 
152
166
  /**
153
- * The derived selector's key selector, memoized on the derived selector itself.
154
- *
155
- * This used to be a second `createCachedSelector` over `[higherOrderSelector]`,
156
- * which meant every key computation paid a whole extra cached-selector layer —
157
- * its own key computation, its own cache lookup, and its own evaluation of
158
- * `higherOrderSelector` — to produce a value that depends on nothing but the
159
- * derived selector. A `WeakMap` keyed on that selector is the same mapping with
160
- * none of the layer, and it shares across cache keys rather than per key.
161
- *
162
- * Note what is deliberately *not* memoized: the `(state, props)` pair. `props`
163
- * is mutated in place by `createBoundSelector`, so two calls can pass the same
164
- * object reference with different contents, and a reference-keyed cache would
165
- * hand back a stale selector.
167
+ * Assigned through a local and written to the selector once. Assigning the
168
+ * property in both branches instead reads as two expando declarations of
169
+ * `keySelector` to TypeScript before 7, which rejects it as a duplicate
170
+ * identifier.
166
171
  */
167
- const derivedKeySelectors = new WeakMap<object, KeySelector<S1 & S2>>();
168
-
169
- combinedSelector.keySelector = (state: S1 & S2, props: P1 & P2) => {
170
- const derivedSelector: any = (higherOrderSelector as any)(state, props);
171
-
172
- let derivedKeySelector = derivedKeySelectors.get(derivedSelector);
173
-
174
- if (derivedKeySelector === undefined) {
175
- derivedKeySelector = keySelectorCreator({
176
- inputSelectors: [higherOrderSelector, derivedSelector],
177
- }) as KeySelector<S1 & S2>;
172
+ let levelKeySelector: KeySelector<any>;
173
+
174
+ if (isDerivedKeyless) {
175
+ /**
176
+ * A `map` level derives a plain `() => output`, never a cached selector, so
177
+ * it contributes nothing to the composed key and the whole composition is
178
+ * known at build time. It is the same call the wrapper below makes, minus
179
+ * the derived selector — so the key is composed exactly as before, once,
180
+ * instead of on every key computation of every level above.
181
+ */
182
+ levelKeySelector = keySelectorCreator({
183
+ inputSelectors: [higherOrderSelector],
184
+ }) as KeySelector<any>;
185
+ } else {
186
+ /**
187
+ * The derived selector's key selector, memoized on the derived selector itself.
188
+ *
189
+ * This used to be a second `createCachedSelector` over `[higherOrderSelector]`,
190
+ * which meant every key computation paid a whole extra cached-selector layer —
191
+ * its own key computation, its own cache lookup, and its own evaluation of
192
+ * `higherOrderSelector` — to produce a value that depends on nothing but the
193
+ * derived selector. A `WeakMap` keyed on that selector is the same mapping with
194
+ * none of the layer, and it shares across cache keys rather than per key.
195
+ *
196
+ * Note what is deliberately *not* memoized: the `(state, props)` pair. `props`
197
+ * is mutated in place by `createBoundSelector`, so two calls can pass the same
198
+ * object reference with different contents, and a reference-keyed cache would
199
+ * hand back a stale selector.
200
+ */
201
+ const derivedKeySelectors = new WeakMap<object, KeySelector<any>>();
202
+
203
+ levelKeySelector = (state: any, props: any) => {
204
+ const derivedSelector: any = (higherOrderSelector as any)(state, props);
205
+
206
+ let derivedKeySelector = derivedKeySelectors.get(derivedSelector);
207
+
208
+ if (derivedKeySelector === undefined) {
209
+ derivedKeySelector = keySelectorCreator({
210
+ inputSelectors: [higherOrderSelector, derivedSelector],
211
+ }) as KeySelector<any>;
212
+
213
+ derivedKeySelectors.set(derivedSelector, derivedKeySelector);
214
+ }
178
215
 
179
- derivedKeySelectors.set(derivedSelector, derivedKeySelector);
180
- }
216
+ return (derivedKeySelector as any)(state, props);
217
+ };
218
+ }
181
219
 
182
- return (derivedKeySelector as any)(state, props);
183
- };
220
+ /**
221
+ * When nothing keyed is left the composition yields the default key, and
222
+ * leaving the property off is what lets the next level skip its cache too.
223
+ * The wrapper above is a fresh closure, so only a `map` level can land here
224
+ * with the default.
225
+ */
226
+ if (levelKeySelector !== defaultKeySelector) {
227
+ combinedSelector.keySelector = levelKeySelector;
228
+ }
184
229
 
185
230
  withDebugName(combinedSelector, () => {
186
231
  const baseName = getSelectorName(selector);
@@ -192,15 +237,15 @@ export function createChainSelector<S1, P1, R1>(
192
237
  parentChain: prevChain,
193
238
  });
194
239
 
195
- return createChainSelector<S1 & S2, P1 & P2, R1 & R2>(
196
- combinedSelector,
197
- combinedOptions,
198
- nextPrevChain,
199
- );
240
+ return createChainSelector(combinedSelector, combinedOptions, nextPrevChain);
241
+ };
242
+
243
+ const chain = (fn: any, chainOptions?: ChainSelectorOptions) => {
244
+ return createLevel(fn, chainOptions, false);
200
245
  };
201
246
 
202
- const map = <R2>(fn: (result: R1) => R2, mapOptions?: ChainSelectorOptions) => {
203
- const mapSelector = (result: R1) => {
247
+ const map = (fn: (result: any) => any, mapOptions?: ChainSelectorOptions) => {
248
+ const mapSelector = (result: any) => {
204
249
  const output = fn(result);
205
250
  const mapSelector = () => output;
206
251
 
@@ -213,7 +258,7 @@ export function createChainSelector<S1, P1, R1>(
213
258
  return mapSelector;
214
259
  };
215
260
 
216
- return chain(mapSelector, mapOptions);
261
+ return createLevel(mapSelector, mapOptions, true);
217
262
  };
218
263
 
219
264
  const build = () => {