Skip to content

Commit ea8081f

Browse files
author
ben.durrant
committed
create EnhancerArray class for proper inference of enhancer extensions
1 parent a636751 commit ea8081f

File tree

7 files changed

+194
-14
lines changed

7 files changed

+194
-14
lines changed

packages/toolkit/src/configureStore.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
ExtractDispatchExtensions,
2626
ExtractStoreExtensions,
2727
} from './tsHelpers'
28+
import { EnhancerArray } from './utils'
2829

2930
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
3031

@@ -34,8 +35,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production'
3435
* @public
3536
*/
3637
export type ConfigureEnhancersCallback<E extends Enhancers = Enhancers> = (
37-
defaultEnhancers: readonly StoreEnhancer[]
38-
) => [...E]
38+
defaultEnhancers: EnhancerArray<[StoreEnhancer<{}, {}>]>
39+
) => E
3940

4041
/**
4142
* Options for `configureStore()`.
@@ -107,7 +108,7 @@ type Enhancers = ReadonlyArray<StoreEnhancer>
107108
export interface ToolkitStore<
108109
S = any,
109110
A extends Action = AnyAction,
110-
M extends Middlewares<S> = Middlewares<S>,
111+
M extends Middlewares<S> = Middlewares<S>
111112
> extends Store<S, A> {
112113
/**
113114
* The `dispatch` method of your store, enhanced by all its middlewares.
@@ -197,12 +198,13 @@ export function configureStore<
197198
})
198199
}
199200

200-
let storeEnhancers: Enhancers = [middlewareEnhancer]
201+
const defaultEnhancers = new EnhancerArray(middlewareEnhancer)
202+
let storeEnhancers: Enhancers = defaultEnhancers
201203

202204
if (Array.isArray(enhancers)) {
203205
storeEnhancers = [middlewareEnhancer, ...enhancers]
204206
} else if (typeof enhancers === 'function') {
205-
storeEnhancers = enhancers(storeEnhancers)
207+
storeEnhancers = enhancers(defaultEnhancers)
206208
}
207209

208210
const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer<any>

packages/toolkit/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export type {
103103
// types
104104
ActionReducerMapBuilder,
105105
} from './mapBuilders'
106-
export { MiddlewareArray } from './utils'
106+
export { MiddlewareArray, EnhancerArray } from './utils'
107107

108108
export { createEntityAdapter } from './entities/create_adapter'
109109
export type {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { configureStore } from '@reduxjs/toolkit'
2+
import type { StoreEnhancer } from 'redux'
3+
4+
declare const expectType: <T>(t: T) => T
5+
6+
declare const enhancer1: StoreEnhancer<{
7+
has1: true
8+
}>
9+
10+
declare const enhancer2: StoreEnhancer<{
11+
has2: true
12+
}>
13+
14+
{
15+
// prepend single element
16+
{
17+
const store = configureStore({
18+
reducer: () => 0,
19+
enhancers: (dE) => dE.prepend(enhancer1),
20+
})
21+
expectType<true>(store.has1)
22+
23+
// @ts-expect-error
24+
expectType<true>(store.has2)
25+
}
26+
27+
// prepend multiple (rest)
28+
{
29+
const store = configureStore({
30+
reducer: () => 0,
31+
enhancers: (dE) => dE.prepend(enhancer1, enhancer2),
32+
})
33+
expectType<true>(store.has1)
34+
expectType<true>(store.has2)
35+
36+
// @ts-expect-error
37+
expectType<true>(store.has3)
38+
}
39+
40+
// prepend multiple (array notation)
41+
{
42+
const store = configureStore({
43+
reducer: () => 0,
44+
enhancers: (dE) => dE.prepend([enhancer1, enhancer2] as const),
45+
})
46+
expectType<true>(store.has1)
47+
expectType<true>(store.has2)
48+
49+
// @ts-expect-error
50+
expectType<true>(store.has3)
51+
}
52+
53+
// concat single element
54+
{
55+
const store = configureStore({
56+
reducer: () => 0,
57+
enhancers: (dE) => dE.concat(enhancer1),
58+
})
59+
expectType<true>(store.has1)
60+
61+
// @ts-expect-error
62+
expectType<true>(store.has2)
63+
}
64+
65+
// prepend multiple (rest)
66+
{
67+
const store = configureStore({
68+
reducer: () => 0,
69+
enhancers: (dE) => dE.concat(enhancer1, enhancer2),
70+
})
71+
expectType<true>(store.has1)
72+
expectType<true>(store.has2)
73+
74+
// @ts-expect-error
75+
expectType<true>(store.has3)
76+
}
77+
78+
// concat multiple (array notation)
79+
{
80+
const store = configureStore({
81+
reducer: () => 0,
82+
enhancers: (dE) => dE.concat([enhancer1, enhancer2] as const),
83+
})
84+
expectType<true>(store.has1)
85+
expectType<true>(store.has2)
86+
87+
// @ts-expect-error
88+
expectType<true>(store.has3)
89+
}
90+
91+
// concat and prepend
92+
{
93+
const store = configureStore({
94+
reducer: () => 0,
95+
enhancers: (dE) => dE.concat(enhancer1).prepend(enhancer2),
96+
})
97+
expectType<true>(store.has1)
98+
expectType<true>(store.has2)
99+
100+
// @ts-expect-error
101+
expectType<true>(store.has3)
102+
}
103+
}

packages/toolkit/src/tests/configureStore.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,7 @@ describe('configureStore', () => {
231231

232232
const store = configureStore({
233233
reducer,
234-
enhancers: (defaultEnhancers) => {
235-
return [...defaultEnhancers, dummyEnhancer]
236-
},
234+
enhancers: (defaultEnhancers) => defaultEnhancers.concat(dummyEnhancer),
237235
})
238236

239237
expect(dummyEnhancerCalled).toBe(true)

packages/toolkit/src/tests/configureStore.typetest.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ const _anyMiddleware: any = () => () => () => {}
194194
)
195195
expectType<string>(store.someProperty)
196196
expectType<number>(store.anotherProperty)
197+
198+
const storeWithCallback = configureStore({
199+
reducer: () => 0,
200+
enhancers: (defaultEnhancers) =>
201+
defaultEnhancers
202+
.prepend(anotherPropertyStoreEnhancer)
203+
.concat(somePropertyStoreEnhancer),
204+
})
205+
206+
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
207+
store.dispatch
208+
)
209+
expectType<string>(storeWithCallback.someProperty)
210+
expectType<number>(storeWithCallback.anotherProperty)
197211
}
198212
}
199213

packages/toolkit/src/tsHelpers.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Middleware, StoreEnhancer } from 'redux'
2-
import type { MiddlewareArray } from './utils'
2+
import type { EnhancerArray, MiddlewareArray } from './utils'
33

44
/**
55
* return True if T is `any`, otherwise return False
@@ -101,9 +101,29 @@ export type ExtractDispatchExtensions<M> = M extends MiddlewareArray<
101101
? ExtractDispatchFromMiddlewareTuple<[...M], {}>
102102
: never
103103

104-
export type ExtractStoreExtensions<E> = E extends any[]
105-
? UnionToIntersection<E[number] extends StoreEnhancer<infer Ext> ? Ext extends {} ? Ext : {} : {}>
106-
: {}
104+
type ExtractStoreExtensionsFromEnhancerTuple<
105+
EnhancerTuple extends any[],
106+
Acc extends {}
107+
> = EnhancerTuple extends [infer Head, ...infer Tail]
108+
? ExtractStoreExtensionsFromEnhancerTuple<
109+
Tail,
110+
Acc & (Head extends StoreEnhancer<infer Ext> ? IsAny<Ext, {}, Ext> : {})
111+
>
112+
: Acc
113+
114+
export type ExtractStoreExtensions<E> = E extends EnhancerArray<
115+
infer EnhancerTuple
116+
>
117+
? ExtractStoreExtensionsFromEnhancerTuple<EnhancerTuple, {}>
118+
: E extends ReadonlyArray<StoreEnhancer>
119+
? UnionToIntersection<
120+
E[number] extends StoreEnhancer<infer Ext>
121+
? Ext extends {}
122+
? Ext
123+
: {}
124+
: {}
125+
>
126+
: never
107127

108128
/**
109129
* Helper type. Passes T out again, but boxes it in a way that it cannot

packages/toolkit/src/utils.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import createNextState, { isDraftable } from 'immer'
2-
import type { Middleware } from 'redux'
2+
import type { Middleware, StoreEnhancer } from 'redux'
33

44
export function getTimeMeasureUtils(maxDelay: number, fnName: string) {
55
let elapsed = 0
@@ -70,6 +70,49 @@ export class MiddlewareArray<
7070
}
7171
}
7272

73+
/**
74+
* @public
75+
*/
76+
export class EnhancerArray<
77+
Enhancers extends StoreEnhancer<any, any>[]
78+
> extends Array<Enhancers[number]> {
79+
constructor(...items: Enhancers)
80+
constructor(...args: any[]) {
81+
super(...args)
82+
Object.setPrototypeOf(this, EnhancerArray.prototype)
83+
}
84+
85+
static get [Symbol.species]() {
86+
return EnhancerArray as any
87+
}
88+
89+
concat<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
90+
items: AdditionalEnhancers
91+
): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]>
92+
93+
concat<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
94+
...items: AdditionalEnhancers
95+
): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]>
96+
concat(...arr: any[]) {
97+
return super.concat.apply(this, arr)
98+
}
99+
100+
prepend<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
101+
items: AdditionalEnhancers
102+
): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]>
103+
104+
prepend<AdditionalEnhancers extends ReadonlyArray<StoreEnhancer<any, any>>>(
105+
...items: AdditionalEnhancers
106+
): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]>
107+
108+
prepend(...arr: any[]) {
109+
if (arr.length === 1 && Array.isArray(arr[0])) {
110+
return new EnhancerArray(...arr[0].concat(this))
111+
}
112+
return new EnhancerArray(...arr.concat(this))
113+
}
114+
}
115+
73116
export function freezeDraftable<T>(val: T) {
74117
return isDraftable(val) ? createNextState(val, () => {}) : val
75118
}

0 commit comments

Comments
 (0)