diff --git a/docs/api/actionCreatorMiddleware.mdx b/docs/api/actionCreatorMiddleware.mdx index 2248a16482..0afce4f368 100644 --- a/docs/api/actionCreatorMiddleware.mdx +++ b/docs/api/actionCreatorMiddleware.mdx @@ -47,6 +47,7 @@ export default function (state = {}, action: any) { import { configureStore, createActionCreatorInvariantMiddleware, + Tuple, } from '@reduxjs/toolkit' import reducer from './reducer' @@ -62,6 +63,6 @@ const actionCreatorMiddleware = createActionCreatorInvariantMiddleware({ const store = configureStore({ reducer, - middleware: [actionCreatorMiddleware], + middleware: new Tuple(actionCreatorMiddleware), }) ``` diff --git a/docs/api/configureStore.mdx b/docs/api/configureStore.mdx index 7abd615508..4d5f9afab3 100644 --- a/docs/api/configureStore.mdx +++ b/docs/api/configureStore.mdx @@ -94,6 +94,22 @@ and should return a middleware array. For more details on how the `middleware` parameter works and the list of middleware that are added by default, see the [`getDefaultMiddleware` docs page](./getDefaultMiddleware.mdx). +:::note Tuple +Typescript users are required to use a `Tuple` instance (if not using a `getDefaultMiddleware` result, which is already a `Tuple`), for better inference. + +```ts no-transpile +import { configureStore, Tuple } from '@reduxjs/toolkit' + +configureStore({ + reducer: rootReducer, + middleware: new Tuple(additionalMiddleware, logger), +}) +``` + +Javascript-only users are free to use a plain array if preferred. + +::: + ### `devTools` If this is a boolean, it will be used to indicate whether `configureStore` should automatically enable support for [the Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools). @@ -122,7 +138,7 @@ If defined as an array, these will be passed to [the Redux `compose` function](h This should _not_ include `applyMiddleware()` or the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`. -Example: `enhancers: [offline]` will result in a final setup of `[applyMiddleware, offline, devToolsExtension]`. +Example: `enhancers: new Tuple(offline)` will result in a final setup of `[applyMiddleware, offline, devToolsExtension]`. If defined as a callback function, it will be called with the existing array of enhancers _without_ the DevTools Extension (currently `[applyMiddleware]`), and should return a new array of enhancers. This is primarily useful for cases where a store enhancer needs to be added @@ -131,6 +147,22 @@ in front of `applyMiddleware`, such as `redux-first-router` or `redux-offline`. Example: `enhancers: (defaultEnhancers) => defaultEnhancers.prepend(offline)` will result in a final setup of `[offline, applyMiddleware, devToolsExtension]`. +:::note Tuple +Typescript users are required to use a `Tuple` instance (if not using a `getDefaultEnhancer` result, which is already a `Tuple`), for better inference. + +```ts no-transpile +import { configureStore, Tuple } from '@reduxjs/toolkit' + +configureStore({ + reducer: rootReducer, + enhancers: new Tuple(offline), +}) +``` + +Javascript-only users are free to use a plain array if preferred. + +::: + ## Usage ### Basic Example diff --git a/docs/api/getDefaultMiddleware.mdx b/docs/api/getDefaultMiddleware.mdx index 370b51ef75..0af30a10e7 100644 --- a/docs/api/getDefaultMiddleware.mdx +++ b/docs/api/getDefaultMiddleware.mdx @@ -28,7 +28,7 @@ If you want to customize the list of middleware, you can supply an array of midd ```js const store = configureStore({ reducer: rootReducer, - middleware: [thunk, logger], + middleware: new Tuple(thunk, logger), }) // Store specifically has the thunk and logger middleware applied @@ -55,7 +55,7 @@ const store = configureStore({ // Store has all of the default middleware added, _plus_ the logger middleware ``` -It is preferable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances. +It is preferable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `Tuple` instead of the array spread operator, as the latter can lose valuable type information under some circumstances. ## Included Default Middleware diff --git a/docs/api/immutabilityMiddleware.mdx b/docs/api/immutabilityMiddleware.mdx index 73b03d972c..16950b129e 100644 --- a/docs/api/immutabilityMiddleware.mdx +++ b/docs/api/immutabilityMiddleware.mdx @@ -74,6 +74,7 @@ export default exampleSlice.reducer import { configureStore, createImmutableStateInvariantMiddleware, + Tuple, } from '@reduxjs/toolkit' import exampleSliceReducer from './exampleSlice' @@ -85,7 +86,7 @@ const immutableInvariantMiddleware = createImmutableStateInvariantMiddleware({ const store = configureStore({ reducer: exampleSliceReducer, // Note that this will replace all default middleware - middleware: [immutableInvariantMiddleware], + middleware: new Tuple(immutableInvariantMiddleware), }) ``` diff --git a/docs/api/serializabilityMiddleware.mdx b/docs/api/serializabilityMiddleware.mdx index 0aa074a65c..e8cb8e8363 100644 --- a/docs/api/serializabilityMiddleware.mdx +++ b/docs/api/serializabilityMiddleware.mdx @@ -93,6 +93,7 @@ import { configureStore, createSerializableStateInvariantMiddleware, isPlain, + Tuple, } from '@reduxjs/toolkit' import reducer from './reducer' @@ -110,7 +111,7 @@ const serializableMiddleware = createSerializableStateInvariantMiddleware({ const store = configureStore({ reducer, - middleware: [serializableMiddleware], + middleware: new Tuple(serializableMiddleware), }) ``` diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md index d4d1a6348d..9fb7f9ab43 100644 --- a/docs/usage/usage-with-typescript.md +++ b/docs/usage/usage-with-typescript.md @@ -99,7 +99,7 @@ export default store The type of the `dispatch` function type will be directly inferred from the `middleware` option. So if you add _correctly typed_ middlewares, `dispatch` should already be correctly typed. -As TypeScript often widens array types when combining arrays using the spread operator, we suggest using the `.concat(...)` and `.prepend(...)` methods of the `MiddlewareArray` returned by `getDefaultMiddleware()`. +As TypeScript often widens array types when combining arrays using the spread operator, we suggest using the `.concat(...)` and `.prepend(...)` methods of the `Tuple` returned by `getDefaultMiddleware()`. ```ts import { configureStore } from '@reduxjs/toolkit' @@ -134,25 +134,18 @@ export type AppDispatch = typeof store.dispatch export default store ``` -#### Using `MiddlewareArray` without `getDefaultMiddleware` +#### Using `Tuple` without `getDefaultMiddleware` -If you want to skip the usage of `getDefaultMiddleware` altogether, you can still use `MiddlewareArray` for type-safe concatenation of your `middleware` array. This class extends the default JavaScript `Array` type, only with modified typings for `.concat(...)` and the additional `.prepend(...)` method. +If you want to skip the usage of `getDefaultMiddleware` altogether, you are required to use `Tuple` for type-safe creation of your `middleware` array. This class extends the default JavaScript `Array` type, only with modified typings for `.concat(...)` and the additional `.prepend(...)` method. -This is generally not required though, as you will probably not run into any array-type-widening issues as long as you are using `as const` and do not use the spread operator. - -So the following two calls would be equivalent: +For example: ```ts -import { configureStore, MiddlewareArray } from '@reduxjs/toolkit' - -configureStore({ - reducer: rootReducer, - middleware: new MiddlewareArray().concat(additionalMiddleware, logger), -}) +import { configureStore, Tuple } from '@reduxjs/toolkit' configureStore({ reducer: rootReducer, - middleware: [additionalMiddleware, logger] as const, + middleware: new Tuple(additionalMiddleware, logger), }) ``` diff --git a/packages/toolkit/src/configureStore.ts b/packages/toolkit/src/configureStore.ts index f08130e75a..44a06894f2 100644 --- a/packages/toolkit/src/configureStore.ts +++ b/packages/toolkit/src/configureStore.ts @@ -23,7 +23,7 @@ import type { ExtractStoreExtensions, ExtractStateExtensions, } from './tsHelpers' -import type { EnhancerArray, MiddlewareArray } from './utils' +import type { Tuple } from './utils' import type { GetDefaultEnhancers } from './getDefaultEnhancers' import { buildGetDefaultEnhancers } from './getDefaultEnhancers' @@ -37,8 +37,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production' export interface ConfigureStoreOptions< S = any, A extends Action = AnyAction, - M extends Middlewares = Middlewares, - E extends Enhancers = Enhancers, + M extends Tuple> = Tuple>, + E extends Tuple = Tuple, P = S > { /** @@ -48,8 +48,8 @@ export interface ConfigureStoreOptions< reducer: Reducer | ReducersMapObject /** - * An array of Redux middleware to install. If not supplied, defaults to - * the set of middleware returned by `getDefaultMiddleware()`. + * An array of Redux middleware to install, or a callback receiving `getDefaultMiddleware` and returning a Tuple of middleware. + * If not supplied, defaults to the set of middleware returned by `getDefaultMiddleware()`. * * @example `middleware: (gDM) => gDM().concat(logger, apiMiddleware, yourCustomMiddleware)` * @see https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage @@ -78,8 +78,8 @@ export interface ConfigureStoreOptions< * The store enhancers to apply. See Redux's `createStore()`. * All enhancers will be included before the DevTools Extension enhancer. * If you need to customize the order of enhancers, supply a callback - * function that will receive a `getDefaultEnhancers` function that returns an EnhancerArray, - * and should return a new array (such as `getDefaultEnhancers().concat(offline)`). + * function that will receive a `getDefaultEnhancers` function that returns a Tuple, + * and should return a Tuple of enhancers (such as `getDefaultEnhancers().concat(offline)`). * If you only need to add middleware, you can use the `middleware` parameter instead. */ enhancers?: ((getDefaultEnhancers: GetDefaultEnhancers) => E) | E @@ -112,8 +112,8 @@ export type EnhancedStore< export function configureStore< S = any, A extends Action = AnyAction, - M extends Middlewares = MiddlewareArray<[ThunkMiddlewareFor]>, - E extends Enhancers = EnhancerArray< + M extends Tuple> = Tuple<[ThunkMiddlewareFor]>, + E extends Tuple = Tuple< [StoreEnhancer<{ dispatch: ExtractDispatchExtensions }>, StoreEnhancer] >, P = S diff --git a/packages/toolkit/src/getDefaultEnhancers.ts b/packages/toolkit/src/getDefaultEnhancers.ts index faf27654aa..6d13646482 100644 --- a/packages/toolkit/src/getDefaultEnhancers.ts +++ b/packages/toolkit/src/getDefaultEnhancers.ts @@ -1,7 +1,7 @@ import type { StoreEnhancer } from 'redux' import type { AutoBatchOptions } from './autoBatchEnhancer' import { autoBatchEnhancer } from './autoBatchEnhancer' -import { EnhancerArray } from './utils' +import { Tuple } from './utils' import type { Middlewares } from './configureStore' import type { ExtractDispatchExtensions } from './tsHelpers' @@ -11,7 +11,7 @@ type GetDefaultEnhancersOptions = { export type GetDefaultEnhancers> = ( options?: GetDefaultEnhancersOptions -) => EnhancerArray<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions }>]> +) => Tuple<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions }>]> export const buildGetDefaultEnhancers = >( middlewareEnhancer: StoreEnhancer<{ dispatch: ExtractDispatchExtensions }> @@ -19,7 +19,7 @@ export const buildGetDefaultEnhancers = >( function getDefaultEnhancers(options) { const { autoBatch = true } = options ?? {} - let enhancerArray = new EnhancerArray(middlewareEnhancer) + let enhancerArray = new Tuple(middlewareEnhancer) if (autoBatch) { enhancerArray.push( autoBatchEnhancer(typeof autoBatch === 'object' ? autoBatch : undefined) diff --git a/packages/toolkit/src/getDefaultMiddleware.ts b/packages/toolkit/src/getDefaultMiddleware.ts index 12ce3ad9e8..acfd94d840 100644 --- a/packages/toolkit/src/getDefaultMiddleware.ts +++ b/packages/toolkit/src/getDefaultMiddleware.ts @@ -11,7 +11,7 @@ import { createImmutableStateInvariantMiddleware } from './immutableStateInvaria import type { SerializableStateInvariantMiddlewareOptions } from './serializableStateInvariantMiddleware' import { createSerializableStateInvariantMiddleware } from './serializableStateInvariantMiddleware' import type { ExcludeFromTuple } from './tsHelpers' -import { MiddlewareArray } from './utils' +import { Tuple } from './utils' function isBoolean(x: any): x is boolean { return typeof x === 'boolean' @@ -48,7 +48,7 @@ export type GetDefaultMiddleware = < } >( options?: O -) => MiddlewareArray], never>> +) => Tuple], never>> export const buildGetDefaultMiddleware = (): GetDefaultMiddleware => function getDefaultMiddleware(options) { @@ -59,7 +59,7 @@ export const buildGetDefaultMiddleware = (): GetDefaultMiddleware => actionCreatorCheck = true, } = options ?? {} - let middlewareArray = new MiddlewareArray() + let middlewareArray = new Tuple() if (thunk) { if (isBoolean(thunk)) { diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 6630c94687..f30024161f 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -101,7 +101,7 @@ export type { // types ActionReducerMapBuilder, } from './mapBuilders' -export { MiddlewareArray, EnhancerArray } from './utils' +export { Tuple } from './utils' export { createEntityAdapter } from './entities/create_adapter' export type { diff --git a/packages/toolkit/src/query/tests/helpers.tsx b/packages/toolkit/src/query/tests/helpers.tsx index 94f56b0e0c..d566b4c0fc 100644 --- a/packages/toolkit/src/query/tests/helpers.tsx +++ b/packages/toolkit/src/query/tests/helpers.tsx @@ -5,9 +5,6 @@ import type { Middleware, Store, Reducer, - EnhancerArray, - StoreEnhancer, - ThunkDispatch, } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/query' @@ -218,8 +215,8 @@ export function setupApiStore< }).concat(api.middleware) return tempMiddleware - .concat(...(middleware?.concat ?? [])) - .prepend(...(middleware?.prepend ?? [])) as typeof tempMiddleware + .concat(middleware?.concat ?? []) + .prepend(middleware?.prepend ?? []) as typeof tempMiddleware }, enhancers: (gde) => gde({ diff --git a/packages/toolkit/src/tests/Tuple.typetest.ts b/packages/toolkit/src/tests/Tuple.typetest.ts new file mode 100644 index 0000000000..4beaf28fae --- /dev/null +++ b/packages/toolkit/src/tests/Tuple.typetest.ts @@ -0,0 +1,81 @@ +import { Tuple } from '@reduxjs/toolkit' +import { expectType } from './helpers' + +/** + * Test: compatibility is checked between described types + */ +{ + const stringTuple = new Tuple('') + + expectType>(stringTuple) + + expectType>(stringTuple) + + // @ts-expect-error + expectType>(stringTuple) + + const numberTuple = new Tuple(0, 1) + // @ts-expect-error + expectType>(numberTuple) +} + +/** + * Test: concat is inferred properly + */ +{ + const singleString = new Tuple('') + + expectType>(singleString) + + expectType>(singleString.concat('')) + + expectType>(singleString.concat([''])) +} + +/** + * Test: prepend is inferred properly + */ +{ + const singleString = new Tuple('') + + expectType>(singleString) + + expectType>(singleString.prepend('')) + + expectType>(singleString.prepend([''])) +} + +/** + * Test: push must match existing items + */ +{ + const stringTuple = new Tuple('') + + stringTuple.push('') + + // @ts-expect-error + stringTuple.push(0) +} + +/** + * Test: Tuples can be combined + */ +{ + const stringTuple = new Tuple('') + + const numberTuple = new Tuple(0, 1) + + expectType>(stringTuple.concat(numberTuple)) + + expectType>(stringTuple.prepend(numberTuple)) + + expectType>(numberTuple.concat(stringTuple)) + + expectType>(numberTuple.prepend(stringTuple)) + + // @ts-expect-error + expectType>(stringTuple.prepend(numberTuple)) + + // @ts-expect-error + expectType>(stringTuple.concat(numberTuple)) +} diff --git a/packages/toolkit/src/tests/configureStore.test.ts b/packages/toolkit/src/tests/configureStore.test.ts index f6e6b41031..1c3fbd3766 100644 --- a/packages/toolkit/src/tests/configureStore.test.ts +++ b/packages/toolkit/src/tests/configureStore.test.ts @@ -1,5 +1,6 @@ import { vi } from 'vitest' import type { StoreEnhancer } from '@reduxjs/toolkit' +import { Tuple } from '@reduxjs/toolkit' import type * as Redux from 'redux' import type * as DevTools from '@internal/devtoolsExtension' @@ -108,7 +109,9 @@ describe('configureStore', async () => { describe('given no middleware', () => { it('calls createStore without any middleware', () => { - expect(configureStore({ middleware: [], reducer })).toBeInstanceOf(Object) + expect( + configureStore({ middleware: new Tuple(), reducer }) + ).toBeInstanceOf(Object) expect(redux.applyMiddleware).toHaveBeenCalledWith() expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line expect(redux.createStore).toHaveBeenCalledWith( @@ -171,9 +174,9 @@ describe('configureStore', async () => { it('calls createStore with custom middleware and without default middleware', () => { const thank: Redux.Middleware = (_store) => (next) => (action) => next(action) - expect(configureStore({ middleware: [thank], reducer })).toBeInstanceOf( - Object - ) + expect( + configureStore({ middleware: new Tuple(thank), reducer }) + ).toBeInstanceOf(Object) expect(redux.applyMiddleware).toHaveBeenCalledWith(thank) expect(mockDevtoolsCompose).toHaveBeenCalled() // @remap-prod-remove-line-line expect(redux.createStore).toHaveBeenCalledWith( @@ -194,7 +197,7 @@ describe('configureStore', async () => { expect(getDefaultMiddleware).toEqual(expect.any(Function)) expect(getDefaultMiddleware()).toEqual(expect.any(Array)) - return [thank] + return new Tuple(thank) }) const store = configureStore({ middleware: builder, reducer }) @@ -303,7 +306,7 @@ describe('configureStore', async () => { it('warns if middleware enhancer is excluded from final array when middlewares are provided', () => { const store = configureStore({ reducer, - enhancers: [dummyEnhancer], + enhancers: new Tuple(dummyEnhancer), }) expect(dummyEnhancerCalled).toBe(true) @@ -315,8 +318,8 @@ describe('configureStore', async () => { it("doesn't warn when middleware enhancer is excluded if no middlewares provided", () => { const store = configureStore({ reducer, - middleware: [], - enhancers: [dummyEnhancer], + middleware: new Tuple(), + enhancers: new Tuple(dummyEnhancer), }) expect(dummyEnhancerCalled).toBe(true) diff --git a/packages/toolkit/src/tests/configureStore.typetest.ts b/packages/toolkit/src/tests/configureStore.typetest.ts index 195ee4db8b..5ee4cbec0c 100644 --- a/packages/toolkit/src/tests/configureStore.typetest.ts +++ b/packages/toolkit/src/tests/configureStore.typetest.ts @@ -10,7 +10,7 @@ import type { } from 'redux' import { applyMiddleware, combineReducers } from 'redux' import type { PayloadAction, ConfigureStoreOptions } from '@reduxjs/toolkit' -import { configureStore, createSlice } from '@reduxjs/toolkit' +import { configureStore, createSlice, Tuple } from '@reduxjs/toolkit' import type { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk' import { thunk } from 'redux-thunk' import { expectNotAny, expectType } from './helpers' @@ -67,20 +67,26 @@ const _anyMiddleware: any = () => () => () => {} } /* - * Test: configureStore() accepts middleware array. + * Test: configureStore() accepts Tuple, but not plain array. */ { const middleware: Middleware = (store) => (next) => next configureStore({ reducer: () => 0, + middleware: new Tuple(middleware), + }) + + configureStore({ + reducer: () => 0, + // @ts-expect-error middleware: [middleware], }) configureStore({ reducer: () => 0, // @ts-expect-error - middleware: ['not middleware'], + middleware: new Tuple('not middleware'), }) } @@ -133,13 +139,21 @@ const _anyMiddleware: any = () => () => () => {} } /* - * Test: configureStore() accepts store enhancer. + * Test: configureStore() accepts store Tuple, but not plain array */ { { + const enhancer = applyMiddleware(() => (next) => next) + const store = configureStore({ reducer: () => 0, - enhancers: [applyMiddleware(() => (next) => next)] as const, + enhancers: new Tuple(enhancer), + }) + + const store2 = configureStore({ + reducer: () => 0, + // @ts-expect-error + enhancers: [enhancer], }) expectType>( @@ -150,7 +164,7 @@ const _anyMiddleware: any = () => () => () => {} configureStore({ reducer: () => 0, // @ts-expect-error - enhancers: ['not a store enhancer'], + enhancers: new Tuple('not a store enhancer'), }) { @@ -178,10 +192,10 @@ const _anyMiddleware: any = () => () => () => {} const store = configureStore({ reducer: () => 0, - enhancers: [ + enhancers: new Tuple( somePropertyStoreEnhancer, - anotherPropertyStoreEnhancer, - ] as const, + anotherPropertyStoreEnhancer + ), }) expectType(store.dispatch) @@ -240,11 +254,10 @@ const _anyMiddleware: any = () => () => () => {} const store = configureStore({ reducer: () => ({ aProperty: 0 }), - enhancers: [ + enhancers: new Tuple( someStateExtendingEnhancer, - anotherStateExtendingEnhancer, - // this doesn't work without the as const - ] as const, + anotherStateExtendingEnhancer + ), }) const state = store.getState() @@ -512,7 +525,7 @@ const _anyMiddleware: any = () => () => () => {} { const store = configureStore({ reducer: reducerA, - middleware: [], + middleware: new Tuple(), }) // @ts-expect-error store.dispatch(thunkA()) @@ -525,7 +538,7 @@ const _anyMiddleware: any = () => () => () => {} { const store = configureStore({ reducer: reducerA, - middleware: [thunk] as [ThunkMiddleware], + middleware: new Tuple(thunk as ThunkMiddleware), }) store.dispatch(thunkA()) // @ts-expect-error @@ -537,21 +550,9 @@ const _anyMiddleware: any = () => () => () => {} { const store = configureStore({ reducer: reducerA, - middleware: [] as any as [Middleware<(a: StateA) => boolean, StateA>], - }) - const result: boolean = store.dispatch(5) - // @ts-expect-error - const result2: string = store.dispatch(5) - } - /** - * Test: read-only middleware tuple - */ - { - const store = configureStore({ - reducer: reducerA, - middleware: [] as any as readonly [ - Middleware<(a: StateA) => boolean, StateA> - ], + middleware: new Tuple( + 0 as unknown as Middleware<(a: StateA) => boolean, StateA> + ), }) const result: boolean = store.dispatch(5) // @ts-expect-error @@ -561,11 +562,13 @@ const _anyMiddleware: any = () => () => () => {} * Test: multiple custom middleware */ { - const middleware = [] as any as [ - Middleware<(a: 'a') => 'A', StateA>, - Middleware<(b: 'b') => 'B', StateA>, - ThunkMiddleware - ] + const middleware = [] as any as Tuple< + [ + Middleware<(a: 'a') => 'A', StateA>, + Middleware<(b: 'b') => 'B', StateA>, + ThunkMiddleware + ] + > const store = configureStore({ reducer: reducerA, middleware, diff --git a/packages/toolkit/src/tests/EnhancerArray.typetest.ts b/packages/toolkit/src/tests/getDefaultEnhancers.typetest.ts similarity index 100% rename from packages/toolkit/src/tests/EnhancerArray.typetest.ts rename to packages/toolkit/src/tests/getDefaultEnhancers.typetest.ts diff --git a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts index 7c27357ece..74dcbf5874 100644 --- a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts +++ b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts @@ -14,7 +14,7 @@ import type { ThunkMiddleware } from 'redux-thunk' import { expectType } from './helpers' import { buildGetDefaultMiddleware } from '@internal/getDefaultMiddleware' -import { MiddlewareArray } from '@internal/utils' +import { Tuple } from '@internal/utils' const getDefaultMiddleware = buildGetDefaultMiddleware() @@ -80,7 +80,7 @@ describe('getDefaultMiddleware', () => { thunk: false, }) - expectType>(m2) + expectType>(m2) const dummyMiddleware: Middleware< { @@ -114,7 +114,7 @@ describe('getDefaultMiddleware', () => { const m3 = middleware.concat(dummyMiddleware, dummyMiddleware2) expectType< - MiddlewareArray< + Tuple< [ ThunkMiddleware, Middleware< @@ -220,7 +220,7 @@ it('allows passing options to actionCreatorCheck', () => { expect(actionCreatorCheckWasCalled).toBe(true) }) -describe('MiddlewareArray functionality', () => { +describe('Tuple functionality', () => { const middleware1: Middleware = () => (next) => (action) => next(action) const middleware2: Middleware = () => (next) => (action) => next(action) const defaultMiddleware = getDefaultMiddleware() @@ -232,7 +232,7 @@ describe('MiddlewareArray functionality', () => { // value is prepended expect(prepended).toEqual([middleware1, ...defaultMiddleware]) // returned value is of correct type - expect(prepended).toBeInstanceOf(MiddlewareArray) + expect(prepended).toBeInstanceOf(Tuple) // prepended is a new array expect(prepended).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified @@ -245,7 +245,7 @@ describe('MiddlewareArray functionality', () => { // value is prepended expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware]) // returned value is of correct type - expect(prepended).toBeInstanceOf(MiddlewareArray) + expect(prepended).toBeInstanceOf(Tuple) // prepended is a new array expect(prepended).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified @@ -258,7 +258,7 @@ describe('MiddlewareArray functionality', () => { // value is prepended expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware]) // returned value is of correct type - expect(prepended).toBeInstanceOf(MiddlewareArray) + expect(prepended).toBeInstanceOf(Tuple) // prepended is a new array expect(prepended).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified @@ -271,7 +271,7 @@ describe('MiddlewareArray functionality', () => { // value is concatenated expect(concatenated).toEqual([...defaultMiddleware, middleware1]) // returned value is of correct type - expect(concatenated).toBeInstanceOf(MiddlewareArray) + expect(concatenated).toBeInstanceOf(Tuple) // concatenated is a new array expect(concatenated).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified @@ -288,7 +288,7 @@ describe('MiddlewareArray functionality', () => { middleware2, ]) // returned value is of correct type - expect(concatenated).toBeInstanceOf(MiddlewareArray) + expect(concatenated).toBeInstanceOf(Tuple) // concatenated is a new array expect(concatenated).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified @@ -305,7 +305,7 @@ describe('MiddlewareArray functionality', () => { middleware2, ]) // returned value is of correct type - expect(concatenated).toBeInstanceOf(MiddlewareArray) + expect(concatenated).toBeInstanceOf(Tuple) // concatenated is a new array expect(concatenated).not.toEqual(defaultMiddleware) // defaultMiddleware is not modified diff --git a/packages/toolkit/src/tests/MiddlewareArray.typetest.ts b/packages/toolkit/src/tests/getDefaultMiddleware.typetest.ts similarity index 100% rename from packages/toolkit/src/tests/MiddlewareArray.typetest.ts rename to packages/toolkit/src/tests/getDefaultMiddleware.typetest.ts diff --git a/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts b/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts index 9235c60efc..24c00cacd9 100644 --- a/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts +++ b/packages/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts @@ -10,6 +10,7 @@ import { createSerializableStateInvariantMiddleware, findNonSerializableValue, isPlain, + Tuple, } from '@reduxjs/toolkit' import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware' @@ -100,7 +101,7 @@ describe('serializableStateInvariantMiddleware', () => { const store = configureStore({ reducer, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) const symbol = Symbol.for('SOME_CONSTANT') @@ -147,7 +148,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: ACTION_TYPE }) @@ -207,7 +208,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: ACTION_TYPE }) @@ -254,7 +255,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: ACTION_TYPE }) @@ -298,7 +299,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: ACTION_TYPE }) @@ -322,7 +323,7 @@ describe('serializableStateInvariantMiddleware', () => { const store = configureStore({ reducer: () => ({}), - middleware: [serializableStateMiddleware], + middleware: new Tuple(serializableStateMiddleware), }) expect(numTimesCalled).toBe(0) @@ -347,7 +348,7 @@ describe('serializableStateInvariantMiddleware', () => { it('default value: meta.arg', () => { configureStore({ reducer, - middleware: [createSerializableStateInvariantMiddleware()], + middleware: new Tuple(createSerializableStateInvariantMiddleware()), }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } }) expect(getLog().log).toMatchInlineSnapshot(`""`) @@ -356,11 +357,11 @@ describe('serializableStateInvariantMiddleware', () => { it('default value can be overridden', () => { configureStore({ reducer, - middleware: [ + middleware: new Tuple( createSerializableStateInvariantMiddleware({ ignoredActionPaths: [], - }), - ], + }) + ), }).dispatch({ type: 'test', meta: { arg: nonSerializableValue } }) expect(getLog().log).toMatchInlineSnapshot(` @@ -379,11 +380,11 @@ describe('serializableStateInvariantMiddleware', () => { it('can specify (multiple) different values', () => { configureStore({ reducer, - middleware: [ + middleware: new Tuple( createSerializableStateInvariantMiddleware({ ignoredActionPaths: ['payload', 'meta.arg'], - }), - ], + }) + ), }).dispatch({ type: 'test', payload: { arg: nonSerializableValue }, @@ -396,11 +397,11 @@ describe('serializableStateInvariantMiddleware', () => { it('can specify regexp', () => { configureStore({ reducer, - middleware: [ + middleware: new Tuple( createSerializableStateInvariantMiddleware({ ignoredActionPaths: [/^payload\..*$/], - }), - ], + }) + ), }).dispatch({ type: 'test', payload: { arg: nonSerializableValue }, @@ -424,7 +425,7 @@ describe('serializableStateInvariantMiddleware', () => { const store = configureStore({ reducer: () => ({}), - middleware: [serializableStateMiddleware], + middleware: new Tuple(serializableStateMiddleware), }) expect(numTimesCalled).toBe(0) @@ -487,7 +488,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: ACTION_TYPE }) @@ -506,15 +507,15 @@ describe('serializableStateInvariantMiddleware', () => { const reducer = () => badValue const store = configureStore({ reducer, - middleware: [ + middleware: new Tuple( createSerializableStateInvariantMiddleware({ isSerializable: () => { numTimesCalled++ return true }, ignoreState: true, - }), - ], + }) + ), }) expect(numTimesCalled).toBe(0) @@ -533,7 +534,7 @@ describe('serializableStateInvariantMiddleware', () => { const reducer = () => badValue const store = configureStore({ reducer, - middleware: [ + middleware: new Tuple( createSerializableStateInvariantMiddleware({ isSerializable: () => { numTimesCalled++ @@ -541,8 +542,8 @@ describe('serializableStateInvariantMiddleware', () => { }, ignoreState: true, ignoreActions: true, - }), - ], + }) + ), }) expect(numTimesCalled).toBe(0) @@ -565,7 +566,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ @@ -591,7 +592,7 @@ describe('serializableStateInvariantMiddleware', () => { reducer: { testSlice: reducer, }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) store.dispatch({ type: 'SOME_ACTION' }) @@ -615,7 +616,7 @@ describe('serializableStateInvariantMiddleware', () => { if (action.type === 'SET_STATE') return action.payload return state }, - middleware: [serializableStateInvariantMiddleware], + middleware: new Tuple(serializableStateInvariantMiddleware), }) const state = createNextState([], () => diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 1b331c9962..d08ed5dda0 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -1,5 +1,5 @@ import type { Middleware, StoreEnhancer } from 'redux' -import type { EnhancerArray, MiddlewareArray } from './utils' +import type { Tuple } from './utils' export function safeAssign( target: T, @@ -91,7 +91,7 @@ export type ExcludeFromTuple = T extends [ : Acc type ExtractDispatchFromMiddlewareTuple< - MiddlewareTuple extends any[], + MiddlewareTuple extends readonly any[], Acc extends {} > = MiddlewareTuple extends [infer Head, ...infer Tail] ? ExtractDispatchFromMiddlewareTuple< @@ -100,7 +100,7 @@ type ExtractDispatchFromMiddlewareTuple< > : Acc -export type ExtractDispatchExtensions = M extends MiddlewareArray< +export type ExtractDispatchExtensions = M extends Tuple< infer MiddlewareTuple > ? ExtractDispatchFromMiddlewareTuple @@ -109,7 +109,7 @@ export type ExtractDispatchExtensions = M extends MiddlewareArray< : never type ExtractStoreExtensionsFromEnhancerTuple< - EnhancerTuple extends any[], + EnhancerTuple extends readonly any[], Acc extends {} > = EnhancerTuple extends [infer Head, ...infer Tail] ? ExtractStoreExtensionsFromEnhancerTuple< @@ -118,9 +118,7 @@ type ExtractStoreExtensionsFromEnhancerTuple< > : Acc -export type ExtractStoreExtensions = E extends EnhancerArray< - infer EnhancerTuple -> +export type ExtractStoreExtensions = E extends Tuple ? ExtractStoreExtensionsFromEnhancerTuple : E extends ReadonlyArray ? UnionToIntersection< @@ -133,7 +131,7 @@ export type ExtractStoreExtensions = E extends EnhancerArray< : never type ExtractStateExtensionsFromEnhancerTuple< - EnhancerTuple extends any[], + EnhancerTuple extends readonly any[], Acc extends {} > = EnhancerTuple extends [infer Head, ...infer Tail] ? ExtractStateExtensionsFromEnhancerTuple< @@ -145,9 +143,7 @@ type ExtractStateExtensionsFromEnhancerTuple< > : Acc -export type ExtractStateExtensions = E extends EnhancerArray< - infer EnhancerTuple -> +export type ExtractStateExtensions = E extends Tuple ? ExtractStateExtensionsFromEnhancerTuple : E extends ReadonlyArray ? UnionToIntersection< diff --git a/packages/toolkit/src/utils.ts b/packages/toolkit/src/utils.ts index 3a55bab1a4..2e29b2bc9e 100644 --- a/packages/toolkit/src/utils.ts +++ b/packages/toolkit/src/utils.ts @@ -40,89 +40,47 @@ export function find( return undefined } -/** - * @public - */ -export class MiddlewareArray< - Middlewares extends Middleware[] -> extends Array { - constructor(...items: Middlewares) - constructor(...args: any[]) { - super(...args) - Object.setPrototypeOf(this, MiddlewareArray.prototype) +export class Tuple = []> extends Array< + Items[number] +> { + constructor(length: number) + constructor(...items: Items) + constructor(...items: any[]) { + super(...items) + Object.setPrototypeOf(this, Tuple.prototype) } static get [Symbol.species]() { - return MiddlewareArray as any + return Tuple as any } - concat>>( - items: AdditionalMiddlewares - ): MiddlewareArray<[...Middlewares, ...AdditionalMiddlewares]> - - concat>>( - ...items: AdditionalMiddlewares - ): MiddlewareArray<[...Middlewares, ...AdditionalMiddlewares]> + concat>( + items: Tuple + ): Tuple<[...Items, ...AdditionalItems]> + concat>( + items: AdditionalItems + ): Tuple<[...Items, ...AdditionalItems]> + concat>( + ...items: AdditionalItems + ): Tuple<[...Items, ...AdditionalItems]> concat(...arr: any[]) { return super.concat.apply(this, arr) } - prepend>>( - items: AdditionalMiddlewares - ): MiddlewareArray<[...AdditionalMiddlewares, ...Middlewares]> - - prepend>>( - ...items: AdditionalMiddlewares - ): MiddlewareArray<[...AdditionalMiddlewares, ...Middlewares]> - - prepend(...arr: any[]) { - if (arr.length === 1 && Array.isArray(arr[0])) { - return new MiddlewareArray(...arr[0].concat(this)) - } - return new MiddlewareArray(...arr.concat(this)) - } -} - -/** - * @public - */ -export class EnhancerArray< - Enhancers extends StoreEnhancer[] -> extends Array { - constructor(...items: Enhancers) - constructor(...args: any[]) { - super(...args) - Object.setPrototypeOf(this, EnhancerArray.prototype) - } - - static get [Symbol.species]() { - return EnhancerArray as any - } - - concat>>( - items: AdditionalEnhancers - ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]> - - concat>>( - ...items: AdditionalEnhancers - ): EnhancerArray<[...Enhancers, ...AdditionalEnhancers]> - concat(...arr: any[]) { - return super.concat.apply(this, arr) - } - - prepend>>( - items: AdditionalEnhancers - ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]> - - prepend>>( - ...items: AdditionalEnhancers - ): EnhancerArray<[...AdditionalEnhancers, ...Enhancers]> - + prepend>( + items: Tuple + ): Tuple<[...AdditionalItems, ...Items]> + prepend>( + items: AdditionalItems + ): Tuple<[...AdditionalItems, ...Items]> + prepend>( + ...items: AdditionalItems + ): Tuple<[...AdditionalItems, ...Items]> prepend(...arr: any[]) { if (arr.length === 1 && Array.isArray(arr[0])) { - return new EnhancerArray(...arr[0].concat(this)) + return new Tuple(...arr[0].concat(this)) } - return new EnhancerArray(...arr.concat(this)) + return new Tuple(...arr.concat(this)) } }