From c676a25525bd7e4f2bf407cf1a135f355a2c1d79 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Tue, 23 Apr 2019 07:35:12 -0700 Subject: [PATCH] Infer action types from combineReducers (#3411) * Infer action types from combineReducers This change allows for `combineReducers` to completely infer both the state and action types for its returned reducer. From experience with large TypeScript projects, it's common to see that the action type is not explicitly specified, which results in `AnyAction` in the resulting reducer type. Unfortunately, this will propagate through the type inference for `createStore` resulting in `dispatch` being very weakly typed. This change alone causes a chain reaction of a more correctly (and strongly) typed project with regards to Redux. * Fix formatting issues. --- index.d.ts | 13 +++++++------ test/typescript/reducers.ts | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/index.d.ts b/index.d.ts index c1a16080bf..1cf5a70433 100644 --- a/index.d.ts +++ b/index.d.ts @@ -90,12 +90,13 @@ export type ReducersMapObject = { * @returns A reducer function that invokes every reducer inside the passed * object, and builds a state object with the same shape. */ -export function combineReducers( - reducers: ReducersMapObject -): Reducer -export function combineReducers( - reducers: ReducersMapObject -): Reducer +export function combineReducers>( + reducers: T +): Reducer, InferActionTypes>> + +type InferActionTypes = R extends Reducer ? A : AnyAction +type InferReducerTypes = T extends Record ? R : Reducer +type InferStateType = T extends ReducersMapObject ? S : never /* store */ diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index 29376714f3..eb03eaf6d7 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -42,7 +42,7 @@ function simple() { // Combined reducer also accepts any action. const combined = combineReducers({ sub: reducer }) - let cs: { sub: State } = combined(undefined, { type: 'init' }) + let cs = combined(undefined, { type: 'init' }) cs = combined(cs, { type: 'INCREMENT', count: 10 }) // Combined reducer's state is strictly checked. @@ -110,17 +110,18 @@ function discriminated() { // typings:expect-error s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) - // Combined reducer accepts any action by default which allows to include - // third-party reducers without the need to add their actions to the union. - const combined = combineReducers({ sub: reducer }) + // Combined reducer accepts a union actions types accepted each reducer, + // which can be very permissive for unknown third-party reducers. + const combined = combineReducers({ + sub: reducer, + unknown: (state => state) as Reducer + }) - let cs: { sub: State } = combined(undefined, { type: 'init' }) - cs = combined(cs, { type: 'SOME_OTHER_TYPE' }) + let cs = combined(undefined, { type: 'init' }) + cs = combined(cs, { type: 'SOME_OTHER_TYPE', someField: 'value' }) // Combined reducer can be made to only accept known actions. - const strictCombined = combineReducers<{ sub: State }, MyAction>({ - sub: reducer - }) + const strictCombined = combineReducers({ sub: reducer }) strictCombined(cs, { type: 'INCREMENT' }) // typings:expect-error @@ -179,7 +180,7 @@ function typeGuards() { const combined = combineReducers({ sub: reducer }) - let cs: { sub: State } = combined(undefined, { type: 'init' }) + let cs = combined(undefined, { type: 'init' }) cs = combined(cs, { type: 'INCREMENT', count: 10 }) }