Skip to content

Commit 2954f00

Browse files
appdentimdorr
authored andcommitted
Infer action types from combineReducers (reduxjs#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.
1 parent 0ec197a commit 2954f00

File tree

2 files changed

+18
-16
lines changed

2 files changed

+18
-16
lines changed

index.d.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ export type ReducersMapObject<S = any, A extends Action = Action> = {
9090
* @returns A reducer function that invokes every reducer inside the passed
9191
* object, and builds a state object with the same shape.
9292
*/
93-
export function combineReducers<S>(
94-
reducers: ReducersMapObject<S, any>
95-
): Reducer<S>
96-
export function combineReducers<S, A extends Action = AnyAction>(
97-
reducers: ReducersMapObject<S, A>
98-
): Reducer<S, A>
93+
export function combineReducers<T extends ReducersMapObject<any, any>>(
94+
reducers: T
95+
): Reducer<InferStateType<T>, InferActionTypes<InferReducerTypes<T>>>
96+
97+
type InferActionTypes<R> = R extends Reducer<any, infer A> ? A : AnyAction
98+
type InferReducerTypes<T> = T extends Record<any, infer R> ? R : Reducer
99+
type InferStateType<T> = T extends ReducersMapObject<infer S, any> ? S : never
99100

100101
/* store */
101102

test/typescript/reducers.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function simple() {
4242
// Combined reducer also accepts any action.
4343
const combined = combineReducers({ sub: reducer })
4444

45-
let cs: { sub: State } = combined(undefined, { type: 'init' })
45+
let cs = combined(undefined, { type: 'init' })
4646
cs = combined(cs, { type: 'INCREMENT', count: 10 })
4747

4848
// Combined reducer's state is strictly checked.
@@ -110,17 +110,18 @@ function discriminated() {
110110
// typings:expect-error
111111
s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' })
112112

113-
// Combined reducer accepts any action by default which allows to include
114-
// third-party reducers without the need to add their actions to the union.
115-
const combined = combineReducers({ sub: reducer })
113+
// Combined reducer accepts a union actions types accepted each reducer,
114+
// which can be very permissive for unknown third-party reducers.
115+
const combined = combineReducers({
116+
sub: reducer,
117+
unknown: (state => state) as Reducer
118+
})
116119

117-
let cs: { sub: State } = combined(undefined, { type: 'init' })
118-
cs = combined(cs, { type: 'SOME_OTHER_TYPE' })
120+
let cs = combined(undefined, { type: 'init' })
121+
cs = combined(cs, { type: 'SOME_OTHER_TYPE', someField: 'value' })
119122

120123
// Combined reducer can be made to only accept known actions.
121-
const strictCombined = combineReducers<{ sub: State }, MyAction>({
122-
sub: reducer
123-
})
124+
const strictCombined = combineReducers({ sub: reducer })
124125

125126
strictCombined(cs, { type: 'INCREMENT' })
126127
// typings:expect-error
@@ -179,7 +180,7 @@ function typeGuards() {
179180

180181
const combined = combineReducers({ sub: reducer })
181182

182-
let cs: { sub: State } = combined(undefined, { type: 'init' })
183+
let cs = combined(undefined, { type: 'init' })
183184
cs = combined(cs, { type: 'INCREMENT', count: 10 })
184185
}
185186

0 commit comments

Comments
 (0)