Skip to content

Commit

Permalink
WIP: add prepareAction option to createAction & createSlice
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Jun 22, 2019
1 parent d6e56a6 commit ebe366d
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 26 deletions.
53 changes: 44 additions & 9 deletions src/createAction.ts
Expand Up @@ -12,12 +12,37 @@ export interface PayloadAction<P = any, T extends string = string>
payload: P
}

export type PayloadMetaAction<P = any, M = any, T extends string = string> = ([
M
] extends [never]
? {}
: { meta: M }) &
PayloadAction<P, T>

export type PrepareAction<
OriginalPayload = any,
TargetPayload = OriginalPayload,
Meta = never
> = (
payload: OriginalPayload
) => [Meta] extends [never]
? { payload: TargetPayload }
: { payload: TargetPayload; meta: Meta }

/**
* An action creator that produces actions with a `payload` attribute.
*/
export interface PayloadActionCreator<P = any, T extends string = string> {
export interface PayloadActionCreator<
P = any,
T extends string = string,
Prepare extends PrepareAction<P, any> = PrepareAction<P, P>
> {
(): Action<T>
(payload: P): PayloadAction<P, T>
(payload: P): Prepare extends PrepareAction<any, infer TP, infer M>
? PayloadMetaAction<TP, M, T>
: Prepare extends PrepareAction<any, infer TP>
? PayloadMetaAction<TP>
: never
type: T
}

Expand All @@ -30,20 +55,30 @@ export interface PayloadActionCreator<P = any, T extends string = string> {
*
* @param type The action type to use for created actions.
*/
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> {
export function createAction<
P = any,
T extends string = string,
Prepare extends PrepareAction<P> = PrepareAction<P, P>
>(type: T, prepareAction?: Prepare): PayloadActionCreator<P, T, Prepare> {
//function actionCreator(): Action<T>
//function actionCreator(payload: P): ActionTypeFor<Prepare, T>
function actionCreator(payload?: P): any {
if (prepareAction) {
return {
...prepareAction(
payload! /* TODO: this does not match up with the current signature */
),
type
}
}
return { type, payload }
}

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

actionCreator.type = type

return actionCreator
return actionCreator as any
}

/**
Expand Down
28 changes: 25 additions & 3 deletions src/createReducer.ts
@@ -1,6 +1,6 @@
import createNextState, { Draft } from 'immer'
import { AnyAction, Action, Reducer } from 'redux'

import { PrepareAction } from './createAction'
/**
* Defines a mapping from action types to corresponding action object shapes.
*/
Expand All @@ -25,11 +25,28 @@ export type CaseReducer<S = any, A extends Action = AnyAction> = (
action: A
) => S | void

export type EnhancedReducer<
S = any,
A extends Action = AnyAction,
P extends PrepareAction = PrepareAction
> = {
reducer: CaseReducer<S, A>
prepare: P
}

export function isEnhancedReducer(
caseReducer: CaseReducer | EnhancedReducer | void
): caseReducer is EnhancedReducer {
return !!caseReducer && 'reducer' in caseReducer
}

/**
* A mapping from action types to case reducers for `createReducer()`.
*/
export type CaseReducers<S, AS extends Actions> = {
[T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
[T in keyof AS]: AS[T] extends Action
? CaseReducer<S, AS[T]> | EnhancedReducer<S, AS[T]>
: void
}

/**
Expand Down Expand Up @@ -58,7 +75,12 @@ export function createReducer<
// these two types.
return createNextState(state, (draft: Draft<S>): any => {
const caseReducer = actionsMap[action.type]
return caseReducer ? caseReducer(draft, action) : undefined

return !caseReducer
? undefined
: typeof caseReducer === 'function'
? caseReducer(draft, action)
: caseReducer.reducer(draft, action)
})
}
}
50 changes: 36 additions & 14 deletions src/createSlice.ts
@@ -1,18 +1,39 @@
import { Reducer } from 'redux'
import { createAction, PayloadAction } from './createAction'
import { createReducer, CaseReducers } from './createReducer'
import {
createAction,
PayloadActionCreator,
PayloadAction
} from './createAction'
import {
createReducer,
CaseReducers,
EnhancedReducer,
isEnhancedReducer
} from './createReducer'
import { createSliceSelector, createSelectorName } from './sliceSelector'

/**
* An action creator atttached to a slice.
*/
export type SliceActionCreator<P> = P extends void
export type SliceActionCreator<P> = { type: string } & (P extends void
? () => PayloadAction<void>
: (payload: P) => PayloadAction<P>
: (payload: P) => PayloadAction<P>)

export type MappedSliceActionCreator<P> = P extends EnhancedReducer
? P['prepare'] extends (_: infer OP) => any
? PayloadActionCreator<OP, any, P['prepare']>
: never
: P extends (state: any) => any
? SliceActionCreator<void>
: P extends (state: any, action: infer Action) => any
? Action extends { payload: infer Payload }
? SliceActionCreator<Payload>
: never
: never

export interface Slice<
S = any,
AP extends { [key: string]: any } = { [key: string]: any }
Actions extends { [type: string]: PayloadActionCreator } = {}
> {
/**
* The slice name.
Expand All @@ -28,7 +49,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: Actions

/**
* Selectors for the slice reducer state. `createSlice()` inserts a single
Expand Down Expand Up @@ -72,12 +93,8 @@ export interface CreateSliceOptions<
extraReducers?: CaseReducers<S, any>
}

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)
type MappedActions<CR extends CaseReducers<any, any>> = {
[T in keyof CR]: MappedSliceActionCreator<CR[T]>
}

function getType(slice: string, actionKey: string): string {
Expand All @@ -94,7 +111,7 @@ function getType(slice: string, actionKey: string): string {
*/
export function createSlice<S, CR extends CaseReducers<S, any>>(
options: CreateSliceOptions<S, CR>
): Slice<S, CaseReducerActionPayloads<CR>> {
): Slice<S, MappedActions<CR>> {
const { slice = '', initialState } = options
const reducers = options.reducers || {}
const extraReducers = options.extraReducers || {}
Expand All @@ -110,7 +127,12 @@ export function createSlice<S, CR extends CaseReducers<S, any>>(
const actionMap = actionKeys.reduce(
(map, action) => {
const type = getType(slice, action)
map[action] = createAction(type)
const reducer = reducers[action]
if (isEnhancedReducer(reducer)) {
map[action] = createAction(type, reducer.prepare)
} else {
map[action] = createAction(type)
}
return map
},
{} as any
Expand Down
27 changes: 27 additions & 0 deletions type-tests/files/createAction.typetest.ts
Expand Up @@ -133,3 +133,30 @@ import {
// typings:expect-error
const q: number = increment(1).type
}

/*
* Test: changing payload type with prepareAction
*/
{
const strLenAction = createAction('strLen', (payload: string) => ({
payload: payload.length
}))
const n: number = strLenAction('test').payload

// typings:expect-error
const r: string = strLenAction('test').payload
}

/*
* Test: adding metadata with prepareAction
*/
{
const strLenMetaAction = createAction('strLenMeta', (payload: string) => ({
payload,
meta: payload.length
}))
const n: number = strLenMetaAction('test').meta

// typings:expect-error
const r: string = strLenMetaAction('test').meta
}
35 changes: 35 additions & 0 deletions type-tests/files/createSlice.typetest.ts
Expand Up @@ -73,3 +73,38 @@ import {
// typings:expect-error
counter.actions.multiply('2')
}

/*
* Test: Slice action creator types are inferred for enhanced reducers.
*/
{
const counter = createSlice({
slice: 'counter',
initialState: 0,
reducers: {
strLen: {
reducer: s => s,
prepare: (payload: string) => ({
payload: payload.length
})
},
strLenMeta: {
reducer: s => s,
prepare: (payload: string) => ({
payload,
meta: payload.length
})
}
}
})

const n1: number = counter.actions.strLen('test').payload
const s1: string = counter.actions.strLenMeta('test').payload
const n2: number = counter.actions.strLenMeta('test').meta

// typings:expect-error
const s2: string = counter.actions.strLen('test').payload

// typings:expect-error
const s3: string = counter.actions.strLenMeta('test').meta
}

0 comments on commit ebe366d

Please sign in to comment.