Skip to content

Commit

Permalink
Infer action types from combineReducers (#3411)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
appden authored and timdorr committed Apr 23, 2019
1 parent b918c48 commit c676a25
Show file tree
Hide file tree
Showing 2 changed files with 18 additions and 16 deletions.
13 changes: 7 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ export type ReducersMapObject<S = any, A extends Action = Action> = {
* @returns A reducer function that invokes every reducer inside the passed
* object, and builds a state object with the same shape.
*/
export function combineReducers<S>(
reducers: ReducersMapObject<S, any>
): Reducer<S>
export function combineReducers<S, A extends Action = AnyAction>(
reducers: ReducersMapObject<S, A>
): Reducer<S, A>
export function combineReducers<T extends ReducersMapObject<any, any>>(
reducers: T
): Reducer<InferStateType<T>, InferActionTypes<InferReducerTypes<T>>>

type InferActionTypes<R> = R extends Reducer<any, infer A> ? A : AnyAction
type InferReducerTypes<T> = T extends Record<any, infer R> ? R : Reducer
type InferStateType<T> = T extends ReducersMapObject<infer S, any> ? S : never

/* store */

Expand Down
21 changes: 11 additions & 10 deletions test/typescript/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 })
}

Expand Down

0 comments on commit c676a25

Please sign in to comment.