Skip to content

Commit 0b50f3b

Browse files
phryneasmarkerikson
authored andcommitted
infer action creators from createSlice as PayloadActionCreator (#158)
* infer action creators from `createSlice` as `PayloadActionCreator` * correct `type` of slice PayloadActions to `string` * always return PayloadAction from PayloadActionCreator
1 parent 2eb5b4b commit 0b50f3b

File tree

4 files changed

+81
-39
lines changed

4 files changed

+81
-39
lines changed

src/createAction.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ export interface PayloadAction<P = any, T extends string = string>
1212
payload: P
1313
}
1414

15+
export type Diff<T, U> = T extends U ? never : T;
16+
1517
/**
1618
* An action creator that produces actions with a `payload` attribute.
1719
*/
18-
export interface PayloadActionCreator<P = any, T extends string = string> {
19-
(): Action<T>
20-
(payload: P): PayloadAction<P, T>
21-
type: T
22-
}
20+
export type PayloadActionCreator<P = any, T extends string = string> = { type: T } & (
21+
/*
22+
* The `P` generic is wrapped with a single-element tuple to prevent the
23+
* conditional from being checked distributively, thus preserving unions
24+
* of contra-variant types.
25+
*/
26+
[undefined] extends [P] ? {
27+
(payload?: undefined): PayloadAction<undefined, T>
28+
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
29+
}
30+
: [void] extends [P] ? {
31+
(): PayloadAction<undefined, T>
32+
}
33+
: {
34+
<PT extends P>(payload: PT): PayloadAction<PT, T>
35+
}
36+
);
2337

2438
/**
2539
* A utility function to create an action creator for the given action type
@@ -33,17 +47,15 @@ export interface PayloadActionCreator<P = any, T extends string = string> {
3347
export function createAction<P = any, T extends string = string>(
3448
type: T
3549
): PayloadActionCreator<P, T> {
36-
function actionCreator(): Action<T>
37-
function actionCreator(payload: P): PayloadAction<P, T>
38-
function actionCreator(payload?: P): Action<T> | PayloadAction<P, T> {
50+
function actionCreator(payload?: P): PayloadAction<undefined | P, T> {
3951
return { type, payload }
4052
}
4153

4254
actionCreator.toString = (): T => `${type}` as T
4355

4456
actionCreator.type = type
4557

46-
return actionCreator
58+
return actionCreator as any
4759
}
4860

4961
/**

src/createSlice.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
import { Reducer } from 'redux'
2-
import { createAction, PayloadAction } from './createAction'
2+
import { createAction, PayloadAction, PayloadActionCreator } from './createAction'
33
import { createReducer, CaseReducers } from './createReducer'
44
import { createSliceSelector, createSelectorName } from './sliceSelector'
55

66
/**
77
* An action creator atttached to a slice.
88
*
9-
* The `P` generic is wrapped with a single-element tuple to prevent the
10-
* conditional from being checked distributively, thus preserving unions
11-
* of contra-variant types.
9+
* @deprecated please use PayloadActionCreator directly
1210
*/
13-
export type SliceActionCreator<P> = [P] extends [void]
14-
? () => PayloadAction<void>
15-
: (payload: P) => PayloadAction<P>
11+
export type SliceActionCreator<P> = PayloadActionCreator<P>
1612

1713
export interface Slice<
1814
S = any,
1915
AP extends { [key: string]: any } = { [key: string]: any }
20-
> {
16+
> {
2117
/**
2218
* The slice name.
2319
*/
@@ -32,7 +28,7 @@ export interface Slice<
3228
* Action creators for the types of actions that are handled by the slice
3329
* reducer.
3430
*/
35-
actions: { [type in keyof AP]: SliceActionCreator<AP[type]> }
31+
actions: { [type in keyof AP]: PayloadActionCreator<AP[type]> }
3632

3733
/**
3834
* Selectors for the slice reducer state. `createSlice()` inserts a single
@@ -49,7 +45,7 @@ export interface Slice<
4945
export interface CreateSliceOptions<
5046
S = any,
5147
CR extends CaseReducers<S, any> = CaseReducers<S, any>
52-
> {
48+
> {
5349
/**
5450
* The slice's name. Used to namespace the generated action types and to
5551
* name the selector for retrieving the reducer's state.
@@ -78,10 +74,10 @@ export interface CreateSliceOptions<
7874

7975
type CaseReducerActionPayloads<CR extends CaseReducers<any, any>> = {
8076
[T in keyof CR]: CR[T] extends (state: any) => any
81-
? void
82-
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
83-
? P
84-
: void)
77+
? void
78+
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
79+
? P
80+
: void)
8581
}
8682

8783
function getType(slice: string, actionKey: string): string {

type-tests/files/createAction.typetest.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
AnyAction
88
} from 'redux-starter-kit'
99

10+
function expectType<T>(p: T) { }
11+
1012
/* PayloadAction */
1113

1214
/*
@@ -50,49 +52,48 @@ import {
5052
/* PayloadActionCreator */
5153

5254
/*
53-
* Test: PayloadActionCreator returns Action or PayloadAction depending
55+
* Test: PayloadActionCreator returns correctly typed PayloadAction depending
5456
* on whether a payload is passed.
5557
*/
5658
{
57-
const actionCreator: PayloadActionCreator = Object.assign(
59+
const actionCreator = Object.assign(
5860
(payload?: number) => ({
5961
type: 'action',
6062
payload
6163
}),
6264
{ type: 'action' }
63-
)
64-
65-
let action: Action
66-
let payloadAction: PayloadAction
65+
) as PayloadActionCreator
6766

68-
action = actionCreator()
69-
action = actionCreator(1)
70-
payloadAction = actionCreator(1)
67+
expectType<PayloadAction<number>>(actionCreator(1));
68+
expectType<PayloadAction<undefined>>(actionCreator());
69+
expectType<PayloadAction<undefined>>(actionCreator(undefined));
7170

7271
// typings:expect-error
73-
payloadAction = actionCreator()
72+
expectType<PayloadAction<number>>(actionCreator());
73+
// typings:expect-error
74+
expectType<PayloadAction<undefined>>(actionCreator(1));
7475
}
7576

7677
/*
7778
* Test: PayloadActionCreator is compatible with ActionCreator.
7879
*/
7980
{
80-
const payloadActionCreator: PayloadActionCreator = Object.assign(
81+
const payloadActionCreator = Object.assign(
8182
(payload?: number) => ({
8283
type: 'action',
8384
payload
8485
}),
8586
{ type: 'action' }
86-
)
87+
) as PayloadActionCreator
8788
const actionCreator: ActionCreator<AnyAction> = payloadActionCreator
8889

89-
const payloadActionCreator2: PayloadActionCreator<number> = Object.assign(
90+
const payloadActionCreator2 = Object.assign(
9091
(payload?: number) => ({
9192
type: 'action',
9293
payload: payload || 1
9394
}),
9495
{ type: 'action' }
95-
)
96+
) as PayloadActionCreator<number>
9697

9798
const actionCreator2: ActionCreator<
9899
PayloadAction<number>
@@ -109,7 +110,7 @@ import {
109110
const n: number = increment(1).payload
110111

111112
// typings:expect-error
112-
const s: string = increment(1).payload
113+
increment("").payload
113114
}
114115

115116
/*
@@ -118,7 +119,11 @@ import {
118119
{
119120
const increment = createAction('increment')
120121
const n: number = increment(1).payload
121-
const s: string = increment(1).payload
122+
const s: string = increment("1").payload
123+
124+
// but infers the payload type to be the argument type
125+
// typings:expect-error
126+
const t: string = increment(1).payload
122127
}
123128
/*
124129
* Test: createAction().type is a string literal.

type-tests/files/createSlice.typetest.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,32 @@ import {
7777
// typings:expect-error
7878
counter.actions.multiply('2')
7979
}
80+
81+
82+
83+
/*
84+
* Test: Slice action creator types properties are "string"
85+
*/
86+
{
87+
const counter = createSlice({
88+
slice: 'counter',
89+
initialState: 0,
90+
reducers: {
91+
increment: state => state + 1,
92+
decrement: state => state - 1,
93+
multiply: (state, { payload }: PayloadAction<number | number[]>) =>
94+
Array.isArray(payload)
95+
? payload.reduce((acc, val) => acc * val, state)
96+
: state * payload
97+
}
98+
})
99+
100+
const s: string = counter.actions.increment.type;
101+
const t: string = counter.actions.decrement.type;
102+
const u: string = counter.actions.multiply.type;
103+
104+
// typings:expect-error
105+
const x: "counter/increment" = counter.actions.increment.type;
106+
// typings:expect-error
107+
const y: "increment" = counter.actions.increment.type;
108+
}

0 commit comments

Comments
 (0)