Skip to content

Require usage of Tuple in TS #3460

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

Merged
merged 5 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion docs/api/actionCreatorMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function (state = {}, action: any) {
import {
configureStore,
createActionCreatorInvariantMiddleware,
Tuple,
} from '@reduxjs/toolkit'
import reducer from './reducer'

Expand All @@ -62,6 +63,6 @@ const actionCreatorMiddleware = createActionCreatorInvariantMiddleware({

const store = configureStore({
reducer,
middleware: [actionCreatorMiddleware],
middleware: new Tuple(actionCreatorMiddleware),
})
```
34 changes: 33 additions & 1 deletion docs/api/configureStore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/api/getDefaultMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/api/immutabilityMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default exampleSlice.reducer
import {
configureStore,
createImmutableStateInvariantMiddleware,
Tuple,
} from '@reduxjs/toolkit'

import exampleSliceReducer from './exampleSlice'
Expand All @@ -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),
})
```

Expand Down
3 changes: 2 additions & 1 deletion docs/api/serializabilityMiddleware.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {
configureStore,
createSerializableStateInvariantMiddleware,
isPlain,
Tuple,
} from '@reduxjs/toolkit'
import reducer from './reducer'

Expand All @@ -110,7 +111,7 @@ const serializableMiddleware = createSerializableStateInvariantMiddleware({

const store = configureStore({
reducer,
middleware: [serializableMiddleware],
middleware: new Tuple(serializableMiddleware),
})
```

Expand Down
19 changes: 6 additions & 13 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
})
```

Expand Down
18 changes: 9 additions & 9 deletions packages/toolkit/src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -37,8 +37,8 @@ const IS_PRODUCTION = process.env.NODE_ENV === 'production'
export interface ConfigureStoreOptions<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>,
E extends Enhancers = Enhancers,
M extends Tuple<Middlewares<S>> = Tuple<Middlewares<S>>,
E extends Tuple<Enhancers> = Tuple<Enhancers>,
P = S
> {
/**
Expand All @@ -48,8 +48,8 @@ export interface ConfigureStoreOptions<
reducer: Reducer<S, A, P> | ReducersMapObject<S, A, P>

/**
* 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
Expand Down Expand Up @@ -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<M>) => E) | E
Expand Down Expand Up @@ -112,8 +112,8 @@ export type EnhancedStore<
export function configureStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = MiddlewareArray<[ThunkMiddlewareFor<S>]>,
E extends Enhancers = EnhancerArray<
M extends Tuple<Middlewares<S>> = Tuple<[ThunkMiddlewareFor<S>]>,
E extends Tuple<Enhancers> = Tuple<
[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>, StoreEnhancer]
>,
P = S
Expand Down
6 changes: 3 additions & 3 deletions packages/toolkit/src/getDefaultEnhancers.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -11,15 +11,15 @@ type GetDefaultEnhancersOptions = {

export type GetDefaultEnhancers<M extends Middlewares<any>> = (
options?: GetDefaultEnhancersOptions
) => EnhancerArray<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>]>
) => Tuple<[StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>]>

export const buildGetDefaultEnhancers = <M extends Middlewares<any>>(
middlewareEnhancer: StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>
): GetDefaultEnhancers<M> =>
function getDefaultEnhancers(options) {
const { autoBatch = true } = options ?? {}

let enhancerArray = new EnhancerArray<StoreEnhancer[]>(middlewareEnhancer)
let enhancerArray = new Tuple<StoreEnhancer[]>(middlewareEnhancer)
if (autoBatch) {
enhancerArray.push(
autoBatchEnhancer(typeof autoBatch === 'object' ? autoBatch : undefined)
Expand Down
6 changes: 3 additions & 3 deletions packages/toolkit/src/getDefaultMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -48,7 +48,7 @@ export type GetDefaultMiddleware<S = any> = <
}
>(
options?: O
) => MiddlewareArray<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>
) => Tuple<ExcludeFromTuple<[ThunkMiddlewareFor<S, O>], never>>

export const buildGetDefaultMiddleware = <S = any>(): GetDefaultMiddleware<S> =>
function getDefaultMiddleware(options) {
Expand All @@ -59,7 +59,7 @@ export const buildGetDefaultMiddleware = <S = any>(): GetDefaultMiddleware<S> =>
actionCreatorCheck = true,
} = options ?? {}

let middlewareArray = new MiddlewareArray<Middleware[]>()
let middlewareArray = new Tuple<Middleware[]>()

if (thunk) {
if (isBoolean(thunk)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 2 additions & 5 deletions packages/toolkit/src/query/tests/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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({
Expand Down
81 changes: 81 additions & 0 deletions packages/toolkit/src/tests/Tuple.typetest.ts
Original file line number Diff line number Diff line change
@@ -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<Tuple<[string]>>(stringTuple)

expectType<Tuple<string[]>>(stringTuple)

// @ts-expect-error
expectType<Tuple<[string, string]>>(stringTuple)

const numberTuple = new Tuple(0, 1)
// @ts-expect-error
expectType<Tuple<string[]>>(numberTuple)
}

/**
* Test: concat is inferred properly
*/
{
const singleString = new Tuple('')

expectType<Tuple<[string]>>(singleString)

expectType<Tuple<[string, string]>>(singleString.concat(''))

expectType<Tuple<[string, string]>>(singleString.concat(['']))
}

/**
* Test: prepend is inferred properly
*/
{
const singleString = new Tuple('')

expectType<Tuple<[string]>>(singleString)

expectType<Tuple<[string, string]>>(singleString.prepend(''))

expectType<Tuple<[string, string]>>(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<Tuple<[string, number, number]>>(stringTuple.concat(numberTuple))

expectType<Tuple<[number, number, string]>>(stringTuple.prepend(numberTuple))

expectType<Tuple<[number, number, string]>>(numberTuple.concat(stringTuple))

expectType<Tuple<[string, number, number]>>(numberTuple.prepend(stringTuple))

// @ts-expect-error
expectType<Tuple<[string, number, number]>>(stringTuple.prepend(numberTuple))

// @ts-expect-error
expectType<Tuple<[number, number, string]>>(stringTuple.concat(numberTuple))
}
Loading