Skip to content

Commit

Permalink
infer action creators from createSlice as PayloadActionCreator (#158
Browse files Browse the repository at this point in the history
)

* infer action creators from `createSlice`  as `PayloadActionCreator`

* correct `type` of slice PayloadActions to `string`

* always return PayloadAction from PayloadActionCreator
  • Loading branch information
phryneas authored and markerikson committed Jul 16, 2019
1 parent 2eb5b4b commit 0b50f3b
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 39 deletions.
30 changes: 21 additions & 9 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,28 @@ export interface PayloadAction<P = any, T extends string = string>
payload: P
}

export type Diff<T, U> = T extends U ? never : T;

/**
* An action creator that produces actions with a `payload` attribute.
*/
export interface PayloadActionCreator<P = any, T extends string = string> {
(): Action<T>
(payload: P): PayloadAction<P, T>
type: T
}
export type PayloadActionCreator<P = any, T extends string = string> = { type: T } & (
/*
* The `P` generic is wrapped with a single-element tuple to prevent the
* conditional from being checked distributively, thus preserving unions
* of contra-variant types.
*/
[undefined] extends [P] ? {
(payload?: undefined): PayloadAction<undefined, T>
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
}
: [void] extends [P] ? {
(): PayloadAction<undefined, T>
}
: {
<PT extends P>(payload: PT): PayloadAction<PT, T>
}
);

/**
* A utility function to create an action creator for the given action type
Expand All @@ -33,17 +47,15 @@ export interface PayloadActionCreator<P = any, T extends string = string> {
export function createAction<P = any, T extends string = string>(
type: T
): PayloadActionCreator<P, T> {
function actionCreator(): Action<T>
function actionCreator(payload: P): PayloadAction<P, T>
function actionCreator(payload?: P): Action<T> | PayloadAction<P, T> {
function actionCreator(payload?: P): PayloadAction<undefined | P, T> {
return { type, payload }
}

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

actionCreator.type = type

return actionCreator
return actionCreator as any
}

/**
Expand Down
24 changes: 10 additions & 14 deletions src/createSlice.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import { Reducer } from 'redux'
import { createAction, PayloadAction } from './createAction'
import { createAction, PayloadAction, PayloadActionCreator } from './createAction'
import { createReducer, CaseReducers } from './createReducer'
import { createSliceSelector, createSelectorName } from './sliceSelector'

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

export interface Slice<
S = any,
AP extends { [key: string]: any } = { [key: string]: any }
> {
> {
/**
* The slice name.
*/
Expand All @@ -32,7 +28,7 @@ export interface Slice<
* Action creators for the types of actions that are handled by the slice
* reducer.
*/
actions: { [type in keyof AP]: SliceActionCreator<AP[type]> }
actions: { [type in keyof AP]: PayloadActionCreator<AP[type]> }

/**
* Selectors for the slice reducer state. `createSlice()` inserts a single
Expand All @@ -49,7 +45,7 @@ export interface Slice<
export interface CreateSliceOptions<
S = any,
CR extends CaseReducers<S, any> = CaseReducers<S, any>
> {
> {
/**
* The slice's name. Used to namespace the generated action types and to
* name the selector for retrieving the reducer's state.
Expand Down Expand Up @@ -78,10 +74,10 @@ export interface CreateSliceOptions<

type CaseReducerActionPayloads<CR extends CaseReducers<any, any>> = {
[T in keyof CR]: CR[T] extends (state: any) => any
? void
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
? P
: void)
? void
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
? P
: void)
}

function getType(slice: string, actionKey: string): string {
Expand Down
37 changes: 21 additions & 16 deletions type-tests/files/createAction.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AnyAction
} from 'redux-starter-kit'

function expectType<T>(p: T) { }

/* PayloadAction */

/*
Expand Down Expand Up @@ -50,49 +52,48 @@ import {
/* PayloadActionCreator */

/*
* Test: PayloadActionCreator returns Action or PayloadAction depending
* Test: PayloadActionCreator returns correctly typed PayloadAction depending
* on whether a payload is passed.
*/
{
const actionCreator: PayloadActionCreator = Object.assign(
const actionCreator = Object.assign(
(payload?: number) => ({
type: 'action',
payload
}),
{ type: 'action' }
)

let action: Action
let payloadAction: PayloadAction
) as PayloadActionCreator

action = actionCreator()
action = actionCreator(1)
payloadAction = actionCreator(1)
expectType<PayloadAction<number>>(actionCreator(1));
expectType<PayloadAction<undefined>>(actionCreator());
expectType<PayloadAction<undefined>>(actionCreator(undefined));

// typings:expect-error
payloadAction = actionCreator()
expectType<PayloadAction<number>>(actionCreator());
// typings:expect-error
expectType<PayloadAction<undefined>>(actionCreator(1));
}

/*
* Test: PayloadActionCreator is compatible with ActionCreator.
*/
{
const payloadActionCreator: PayloadActionCreator = Object.assign(
const payloadActionCreator = Object.assign(
(payload?: number) => ({
type: 'action',
payload
}),
{ type: 'action' }
)
) as PayloadActionCreator
const actionCreator: ActionCreator<AnyAction> = payloadActionCreator

const payloadActionCreator2: PayloadActionCreator<number> = Object.assign(
const payloadActionCreator2 = Object.assign(
(payload?: number) => ({
type: 'action',
payload: payload || 1
}),
{ type: 'action' }
)
) as PayloadActionCreator<number>

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

// typings:expect-error
const s: string = increment(1).payload
increment("").payload
}

/*
Expand All @@ -118,7 +119,11 @@ import {
{
const increment = createAction('increment')
const n: number = increment(1).payload
const s: string = increment(1).payload
const s: string = increment("1").payload

// but infers the payload type to be the argument type
// typings:expect-error
const t: string = increment(1).payload
}
/*
* Test: createAction().type is a string literal.
Expand Down
29 changes: 29 additions & 0 deletions type-tests/files/createSlice.typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,32 @@ import {
// typings:expect-error
counter.actions.multiply('2')
}



/*
* Test: Slice action creator types properties are "string"
*/
{
const counter = createSlice({
slice: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1,
multiply: (state, { payload }: PayloadAction<number | number[]>) =>
Array.isArray(payload)
? payload.reduce((acc, val) => acc * val, state)
: state * payload
}
})

const s: string = counter.actions.increment.type;
const t: string = counter.actions.decrement.type;
const u: string = counter.actions.multiply.type;

// typings:expect-error
const x: "counter/increment" = counter.actions.increment.type;
// typings:expect-error
const y: "increment" = counter.actions.increment.type;
}

0 comments on commit 0b50f3b

Please sign in to comment.