Skip to content

Commit a8b3b37

Browse files
committed
Improve type inference of case reducers
Previously, the TypeScript compiler would reject case reducer maps with different incompatible PayloadAction types. The case reducers map and createReducer() / createSlice() types have now been restructured to allow for better type inference. Fixes #131
1 parent 6d1f04a commit a8b3b37

File tree

4 files changed

+33
-42
lines changed

4 files changed

+33
-42
lines changed

src/createReducer.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import createNextState, { Draft } from 'immer'
22
import { AnyAction, Action, Reducer } from 'redux'
33

4+
/**
5+
* Defines a mapping from action types to corresponding action object shapes.
6+
*/
7+
export type Actions<T extends keyof any = string> = Record<T, Action>
8+
49
/**
510
* An *case reducer* is a reducer function for a speficic action type. Case
611
* reducers can be composed to full reducers using `createReducer()`.
@@ -23,8 +28,8 @@ export type CaseReducer<S = any, A extends Action = AnyAction> = (
2328
/**
2429
* A mapping from action types to case reducers for `createReducer()`.
2530
*/
26-
export interface CaseReducersMapObject<S = any, A extends Action = AnyAction> {
27-
[actionType: string]: CaseReducer<S, A>
31+
export type CaseReducers<S, AS extends Actions> = {
32+
[T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
2833
}
2934

3035
/**
@@ -43,17 +48,17 @@ export interface CaseReducersMapObject<S = any, A extends Action = AnyAction> {
4348
* @param actionsMap A mapping from action types to action-type-specific
4449
* case redeucers.
4550
*/
46-
export function createReducer<S = any, A extends Action = AnyAction>(
47-
initialState: S,
48-
actionsMap: CaseReducersMapObject<S, A>
49-
): Reducer<S> {
51+
export function createReducer<
52+
S,
53+
CR extends CaseReducers<S, any> = CaseReducers<S, any>
54+
>(initialState: S, actionsMap: CR): Reducer<S> {
5055
return function(state = initialState, action): S {
5156
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
5257
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
5358
// these two types.
5459
return createNextState(state, (draft: Draft<S>) => {
5560
const caseReducer = actionsMap[action.type]
56-
return caseReducer ? caseReducer(draft, action as A) : undefined
61+
return caseReducer ? caseReducer(draft, action) : undefined
5762
})
5863
}
5964
}

src/createSlice.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { createSlice } from './createSlice'
2-
import { createAction } from './createAction'
2+
import { createAction, PayloadAction } from './createAction'
33

44
describe('createSlice', () => {
55
describe('when slice is empty', () => {
66
const { actions, reducer, selectors } = createSlice({
77
reducers: {
88
increment: state => state + 1,
9-
multiply: (state, action) => state * action.payload
9+
multiply: (state, action: PayloadAction<number>) =>
10+
state * action.payload
1011
},
1112
initialState: 0
1213
})

src/createSlice.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Action, AnyAction, Reducer } from 'redux'
1+
import { Reducer } from 'redux'
22
import { createAction, PayloadAction } from './createAction'
3-
import { createReducer, CaseReducersMapObject } from './createReducer'
3+
import { createReducer, CaseReducers } from './createReducer'
44
import { createSliceSelector, createSelectorName } from './sliceSelector'
55

66
/**
77
* An action creator atttached to a slice.
88
*/
9-
export type SliceActionCreator<P> = (payload: P) => PayloadAction<P>
9+
export type SliceActionCreator<P> = P extends void
10+
? () => PayloadAction<void>
11+
: (payload: P) => PayloadAction<P>
1012

1113
export interface Slice<
1214
S = any,
13-
A extends Action = AnyAction,
1415
AP extends { [key: string]: any } = { [key: string]: any }
1516
> {
1617
/**
@@ -21,7 +22,7 @@ export interface Slice<
2122
/**
2223
* The slice's reducer.
2324
*/
24-
reducer: Reducer<S, A>
25+
reducer: Reducer<S>
2526

2627
/**
2728
* Action creators for the types of actions that are handled by the slice
@@ -43,9 +44,7 @@ export interface Slice<
4344
*/
4445
export interface CreateSliceOptions<
4546
S = any,
46-
A extends Action = AnyAction,
47-
CR extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>,
48-
CR2 extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>
47+
CR extends CaseReducers<S, any> = CaseReducers<S, any>
4948
> {
5049
/**
5150
* The slice's name. Used to namespace the generated action types and to
@@ -70,19 +69,15 @@ export interface CreateSliceOptions<
7069
* functions. These reducers should have existing action types used
7170
* as the keys, and action creators will _not_ be generated.
7271
*/
73-
extraReducers?: CR2
72+
extraReducers?: CaseReducers<S, any>
7473
}
7574

76-
type ExtractPayloads<
77-
S,
78-
A extends PayloadAction,
79-
CR extends CaseReducersMapObject<S, A>
80-
> = {
81-
[type in keyof CR]: CR[type] extends (state: S) => any
75+
type CaseReducerActionPayloads<CR extends CaseReducers<any, any>> = {
76+
[T in keyof CR]: CR[T] extends (state: any) => any
8277
? void
83-
: (CR[type] extends (state: S, action: PayloadAction<infer P>) => any
78+
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
8479
? P
85-
: never)
80+
: void)
8681
}
8782

8883
function getType(slice: string, actionKey: string): string {
@@ -97,13 +92,9 @@ function getType(slice: string, actionKey: string): string {
9792
*
9893
* The `reducer` argument is passed to `createReducer()`.
9994
*/
100-
export function createSlice<
101-
S = any,
102-
A extends PayloadAction = PayloadAction<any>,
103-
CR extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>
104-
>(
105-
options: CreateSliceOptions<S, A, CR>
106-
): Slice<S, A, ExtractPayloads<S, A, CR>> {
95+
export function createSlice<S, CR extends CaseReducers<S, any>>(
96+
options: CreateSliceOptions<S, CR>
97+
): Slice<S, CaseReducerActionPayloads<CR>> {
10798
const { slice = '', initialState } = options
10899
const reducers = options.reducers || {}
109100
const extraReducers = options.extraReducers || {}

type-tests/files/createReducer.typetest.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { AnyAction, createReducer, Reducer } from 'redux-starter-kit'
2323
}
2424

2525
/**
26-
* Test: createReducer() type parameters can be specified expliclity.
26+
* Test: createReducer() state type can be specified expliclity.
2727
*/
2828
{
2929
type CounterAction =
@@ -36,19 +36,13 @@ import { AnyAction, createReducer, Reducer } from 'redux-starter-kit'
3636
const decrementHandler = (state: number, action: CounterAction) =>
3737
state - action.payload
3838

39-
createReducer<number, CounterAction>(0, {
39+
createReducer<number>(0, {
4040
increment: incrementHandler,
4141
decrement: decrementHandler
4242
})
4343

4444
// typings:expect-error
45-
createReducer<string, CounterAction>(0, {
46-
increment: incrementHandler,
47-
decrement: decrementHandler
48-
})
49-
50-
// typings:expect-error
51-
createReducer<number, AnyAction>(0, {
45+
createReducer<string>(0, {
5246
increment: incrementHandler,
5347
decrement: decrementHandler
5448
})

0 commit comments

Comments
 (0)