Skip to content
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

#2979 Add strict type inference overload for combineReducers. #3484

Merged
merged 4 commits into from
Aug 10, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
41 changes: 41 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,44 @@ export type ReducersMapObject<S = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>
}

/**
* Infer a combined state shape from a `ReducersMapObject`.
*
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
*/
export type StateFromReducersMapObject<M> = M extends ReducersMapObject
? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }
: never

/**
* Infer reducer union type from a `ReducersMapObject`.
*
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
*/
export type ReducerFromReducersMapObject<M> = M extends {
[P in keyof M]: infer R
}
? R extends Reducer<any, any>
? R
: never
: never

/**
* Infer action type from a reducer function.
*
* @template R Type of reducer.
*/
export type ActionFromReducer<R> = R extends Reducer<any, infer A> ? A : never

/**
* Infer action union type from a `ReducersMapObject`.
*
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
*/
export type ActionFromReducersMapObject<M> = M extends ReducersMapObject
? ActionFromReducer<ReducerFromReducersMapObject<M>>
: never

/**
* Turns an object whose values are different reducer functions, into a single
* reducer function. It will call every child reducer, and gather their results
Expand All @@ -96,6 +134,9 @@ export function combineReducers<S>(
export function combineReducers<S, A extends Action = AnyAction>(
reducers: ReducersMapObject<S, A>
): Reducer<S, A>
export function combineReducers<M extends ReducersMapObject<any, any>>(
reducers: M
): Reducer<StateFromReducersMapObject<M>, ActionFromReducersMapObject<M>>

/* store */

Expand Down
75 changes: 57 additions & 18 deletions test/typescript/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,21 @@ function discriminated() {
count?: number
}

interface MultiplyAction {
type: 'MULTIPLY'
count?: number
}

interface DivideAction {
type: 'DIVIDE'
count?: number
}

// Union of all actions in the app.
type MyAction = IncrementAction | DecrementAction
type MyAction0 = IncrementAction | DecrementAction
type MyAction1 = MultiplyAction | DivideAction

const reducer: Reducer<State, MyAction> = (state = 0, action) => {
const reducer0: Reducer<State, MyAction0> = (state = 0, action) => {
if (action.type === 'INCREMENT') {
// Action shape is determined by `type` discriminator.
// typings:expect-error
Expand All @@ -94,37 +105,65 @@ function discriminated() {
return state
}

const reducer1: Reducer<State, MyAction1> = (state = 0, action) => {
if (action.type === 'MULTIPLY') {
// typings:expect-error
action.wrongField

const { count = 1 } = action

return state * count
}

if (action.type === 'DIVIDE') {
// typings:expect-error
action.wrongField

const { count = 1 } = action

return state / count
}

return state
}

// Reducer state is initialized by Redux using Init action which is private.
// To initialize manually (e.g. in tests) we have to type cast init action
// or add a custom init action to MyAction union.
let s: State = reducer(undefined, { type: 'init' } as any)
s = reducer(s, { type: 'INCREMENT' })
s = reducer(s, { type: 'INCREMENT', count: 10 })
let s: State = reducer0(undefined, { type: 'init' } as any)
s = reducer0(s, { type: 'INCREMENT' })
s = reducer0(s, { type: 'INCREMENT', count: 10 })
// Known actions are strictly checked.
// typings:expect-error
s = reducer(s, { type: 'DECREMENT', coun: 10 })
s = reducer(s, { type: 'DECREMENT', count: 10 })
s = reducer0(s, { type: 'DECREMENT', coun: 10 })
s = reducer0(s, { type: 'DECREMENT', count: 10 })
// Unknown actions are rejected.
// typings:expect-error
s = reducer(s, { type: 'SOME_OTHER_TYPE' })
s = reducer0(s, { type: 'SOME_OTHER_TYPE' })
// typings:expect-error
s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' })
s = reducer0(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 infers state and actions by default which maintains type
// safety and still allows inclusion of third-party reducers without the need
// to explicitly add their state and actions to the union.
const combined = combineReducers({ sub0: reducer0, sub1: reducer1 })

let cs: { sub: State } = combined(undefined, { type: 'init' })
cs = combined(cs, { type: 'SOME_OTHER_TYPE' })
const cs = combined(undefined, { type: 'INCREMENT' })
combined(cs, { type: 'MULTIPLY' })
// typings:expect-error
combined(cs, { type: 'init' })
// typings:expect-error
combined(cs, { type: 'SOME_OTHER_TYPE' })

// Combined reducer can be made to only accept known actions.
const strictCombined = combineReducers<{ sub: State }, MyAction>({
sub: reducer
const strictCombined = combineReducers<{ sub: State }, MyAction0>({
sub: reducer0
})

strictCombined(cs, { type: 'INCREMENT' })
const scs = strictCombined(undefined, { type: 'INCREMENT' })
strictCombined(scs, { type: 'DECREMENT' })
// typings:expect-error
strictCombined(cs, { type: 'SOME_OTHER_TYPE' })
strictCombined(scs, { type: 'SOME_OTHER_TYPE' })
}

/**
Expand Down