Skip to content

Commit

Permalink
correctly infer dispatch from provided middlewares (#304)
Browse files Browse the repository at this point in the history
* correctly infer dispatch from provided middlewares

* simplify getDefaultMiddleware typings

* test readability

* documentation, update api report

* extract tests for >=3.4

* update documentation

* Tweak TS usage wording

Co-authored-by: Mark Erikson <mark@isquaredsoftware.com>
  • Loading branch information
phryneas and markerikson committed Jan 16, 2020
1 parent 7aea7b0 commit 775da05
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 51 deletions.
42 changes: 30 additions & 12 deletions docs/usage/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,42 @@ const store = configureStore({
export type AppDispatch = typeof store.dispatch
```
### Extending the `Dispatch` type
### Correct typings for the `Dispatch` type
By default, this `AppDispatch` type will account only for the already included `redux-thunk` middleware. If you're adding additional middlewares that provide different return types for some actions, you can overload that `AppDispatch` type. While you can just extend the `AppDispatch` type, it's recommended to do so by adding additional type overloads for `dispatch` on the store, to keep everything more consistent:
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.
```typescript
const _store = configureStore({
/* ... */
})
There might however be cases, where TypeScript decides to simplify your provided middleware array down to just `Array<Middleware>`. In that case, you have to either specify the array type manually as a tuple, or in TS versions >= 3.4, just add `as const` to your definition.
type EnhancedStoreType = {
dispatch(action: MyCustomActionType): MyCustomReturnType
dispatch(action: MyCustomActionType2): MyCustomReturnType2
} & typeof _store
Please note that when calling `getDefaultMiddleware` in TypeScript, you have to provide the state type as a generic argument.
export const store: EnhancedStoreType = _store
export type AppDispatch = typeof store.dispatch
```ts
import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'

type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
reducer: rootReducer,
middleware: [
// getDefaultMiddleware needs to be called with the state type
...getDefaultMiddleware<RootState>(),
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also manually type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
] as const // prevent this from becoming just `Array<Middleware>`
})

type AppDispatch = typeof store.dispatch
```
If you need any additional reference or examples, [the type tests for `configureStore`](https://github.com/reduxjs/redux-toolkit/blob/master/type-tests/files/configureStore.typetest.ts) contain many different scenarios on how to type this.
### Using the extracted `Dispatch` type with React-Redux
By default, the React-Redux `useDispatch` hook does not contain any types that take middlewares into account. If you need a more specific type for the `dispatch` function when dispatching, you may specify the type of the returned `dispatch` function, or create a custom-typed version of `useSelector`. See [the React-Redux documentation](https://react-redux.js.org/using-react-redux/static-typing#typing-the-usedispatch-hook) for details.
Expand Down
20 changes: 12 additions & 8 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AnyAction } from 'redux';
import { default as createNextState } from 'immer';
import { createSelector } from 'reselect';
import { DeepPartial } from 'redux';
import { Dispatch } from 'redux';
import { Draft } from 'immer';
import { EnhancerOptions } from 'redux-devtools-extension';
import { Middleware } from 'redux';
Expand All @@ -17,7 +18,7 @@ import { ReducersMapObject } from 'redux';
import { Store } from 'redux';
import { StoreEnhancer } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { ThunkDispatch } from 'redux-thunk';
import { ThunkMiddleware } from 'redux-thunk';

// @public
export interface ActionCreatorWithNonInferrablePayload<T extends string = string> extends BaseActionCreator<unknown, T> {
Expand Down Expand Up @@ -80,13 +81,13 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
export type ConfigureEnhancersCallback = (defaultEnhancers: StoreEnhancer[]) => StoreEnhancer[];

// @public
export function configureStore<S = any, A extends Action = AnyAction>(options: ConfigureStoreOptions<S, A>): EnhancedStore<S, A>;
export function configureStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = [ThunkMiddleware<S>]>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M>;

// @public
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> {
devTools?: boolean | EnhancerOptions;
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback;
middleware?: Middleware<{}, S>[];
middleware?: M;
preloadedState?: DeepPartial<S extends any ? S : S>;
reducer: Reducer<S, A> | ReducersMapObject<S, A>;
}
Expand Down Expand Up @@ -124,16 +125,19 @@ export interface CreateSliceOptions<State = any, CR extends SliceCaseReducers<St
export { Draft }

// @public
export interface EnhancedStore<S = any, A extends Action = AnyAction> extends Store<S, A> {
// (undocumented)
dispatch: ThunkDispatch<S, any, A>;
export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends Middlewares<S> = Middlewares<S>> extends Store<S, A> {
dispatch: DispatchForMiddlewares<M> & Dispatch<A>;
}

// @public (undocumented)
export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][]): NonSerializableValue | false;

// @public
export function getDefaultMiddleware<S = any>(options?: GetDefaultMiddlewareOptions): Middleware<{}, S>[];
export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddlewareOptions> = {
thunk: true;
immutableCheck: true;
serializableCheck: true;
}>(options?: O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>>;

// @public
export function getType<T extends string>(actionCreator: PayloadActionCreator<any, T>): T;
Expand Down
37 changes: 26 additions & 11 deletions src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import {
AnyAction,
StoreEnhancer,
Store,
DeepPartial
DeepPartial,
Dispatch
} from 'redux'
import {
composeWithDevTools,
EnhancerOptions as DevToolsOptions
} from 'redux-devtools-extension'
import { ThunkDispatch } from 'redux-thunk'
import { ThunkMiddleware } from 'redux-thunk'

import isPlainObject from './isPlainObject'
import { getDefaultMiddleware } from './getDefaultMiddleware'
import { DispatchForMiddlewares } from './tsHelpers'

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

Expand All @@ -37,7 +39,11 @@ export type ConfigureEnhancersCallback = (
*
* @public
*/
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
export interface ConfigureStoreOptions<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>
> {
/**
* A single reducer function that will be used as the root reducer, or an
* object of slice reducers that will be passed to `combineReducers()`.
Expand All @@ -48,7 +54,7 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
* An array of Redux middleware to install. If not supplied, defaults to
* the set of middleware returned by `getDefaultMiddleware()`.
*/
middleware?: Middleware<{}, S>[]
middleware?: M

/**
* Whether to enable Redux DevTools integration. Defaults to `true`.
Expand Down Expand Up @@ -82,18 +88,25 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
}

type Middlewares<S> = ReadonlyArray<Middleware<{}, S>>

/**
* A Redux store returned by `configureStore()`. Supports dispatching
* side-effectful _thunks_ in addition to plain actions.
*
* @public
*/
export interface EnhancedStore<S = any, A extends Action = AnyAction>
extends Store<S, A> {
export interface EnhancedStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>
> extends Store<S, A> {
/**
* The `dispatch` method of your store, enhanced by all it's middlewares.
*
* @inheritdoc
*/
dispatch: ThunkDispatch<S, any, A>
dispatch: DispatchForMiddlewares<M> & Dispatch<A>

This comment has been minimized.

Copy link
@jonjaques

jonjaques Jan 21, 2020

Pretty sure this should be an |; We experienced a type regression w/ the following:

Type 'ThunkAction<any, CombinedState<{ router: RouterState<any>; form: FormStateMap; user: IUser; cameras: IDuckState; users: IUsersState; global: IGlobalState; ... 7 more ...; connectionManager: IConnectManagerState; }>, { ...; }, IAction>' is missing the following properties from type 'LocationChangeAction<any>': type, payload

This comment has been minimized.

Copy link
@markerikson

markerikson Jan 21, 2020

Author Collaborator

Can you file an issue for this? Comments on an individual commit aren't easily findable.

This comment has been minimized.

Copy link
@jonjaques
}

/**
Expand All @@ -104,9 +117,11 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction>
*
* @public
*/
export function configureStore<S = any, A extends Action = AnyAction>(
options: ConfigureStoreOptions<S, A>
): EnhancedStore<S, A> {
export function configureStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = [ThunkMiddleware<S>]
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
const {
reducer = undefined,
middleware = getDefaultMiddleware(),
Expand Down Expand Up @@ -147,7 +162,7 @@ export function configureStore<S = any, A extends Action = AnyAction>(
storeEnhancers = enhancers(storeEnhancers)
}

const composedEnhancer = finalCompose(...storeEnhancers) as StoreEnhancer
const composedEnhancer = finalCompose(...storeEnhancers) as any

return createStore(
rootReducer,
Expand Down
2 changes: 1 addition & 1 deletion src/getDefaultMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createStore, applyMiddleware, AnyAction } from 'redux'
import { AnyAction } from 'redux'
import { getDefaultMiddleware } from './getDefaultMiddleware'
import { configureStore } from './configureStore'
import thunk, { ThunkAction } from 'redux-thunk'
Expand Down
25 changes: 19 additions & 6 deletions src/getDefaultMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Middleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { Middleware, AnyAction } from 'redux'
import thunkMiddleware, { ThunkMiddleware } from 'redux-thunk'
/* PROD_START_REMOVE_UMD */
import createImmutableStateInvariantMiddleware from 'redux-immutable-state-invariant'
/* PROD_STOP_REMOVE_UMD */
Expand Down Expand Up @@ -28,6 +28,14 @@ interface GetDefaultMiddlewareOptions {
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
}

type ThunkMiddlewareFor<S, O extends GetDefaultMiddlewareOptions> = O extends {
thunk: false
}
? never
: O extends { thunk: { extraArgument: infer E } }
? ThunkMiddleware<S, AnyAction, E>
: ThunkMiddleware<S>

/**
* Returns any array containing the default middleware installed by
* `configureStore()`. Useful if you want to configure your store with a custom
Expand All @@ -37,9 +45,14 @@ interface GetDefaultMiddlewareOptions {
*
* @public
*/
export function getDefaultMiddleware<S = any>(
options: GetDefaultMiddlewareOptions = {}
): Middleware<{}, S>[] {
export function getDefaultMiddleware<
S = any,
O extends Partial<GetDefaultMiddlewareOptions> = {
thunk: true
immutableCheck: true
serializableCheck: true
}
>(options: O = {} as O): Array<Middleware<{}, S> | ThunkMiddlewareFor<S, O>> {
const {
thunk = true,
immutableCheck = true,
Expand Down Expand Up @@ -86,5 +99,5 @@ export function getDefaultMiddleware<S = any>(
}
}

return middlewareArray
return middlewareArray as any
}
27 changes: 27 additions & 0 deletions src/tsHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Middleware } from 'redux'

/**
* return True if T is `any`, otherwise return False
* taken from https://github.com/joonhocho/tsdef
Expand Down Expand Up @@ -60,3 +62,28 @@ export type IsUnknownOrNonInferrable<T, True, False> = AtLeastTS35<
IsUnknown<T, True, False>,
IsEmptyObj<T, True, IsUnknown<T, True, False>>
>

/**
* Combines all dispatch signatures of all middlewares in the array `M` into
* one intersected dispatch signature.
*/
export type DispatchForMiddlewares<M> = M extends ReadonlyArray<any>
? UnionToIntersection<
M[number] extends infer MiddlewareValues
? MiddlewareValues extends Middleware<infer DispatchExt, any, any>
? DispatchExt extends Function
? DispatchExt
: never
: never
: never
>
: never

/**
* Convert a Union type `(A|B)` to and intersecion type `(A&B)`
*/
type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends ((k: infer I) => void)
? I
: never
Loading

0 comments on commit 775da05

Please sign in to comment.