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

Add inferred action creator types #975

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/configureStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export interface EnhancedStore<
export function configureStore<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = [ThunkMiddlewareFor<S>]
M extends Middlewares<S> = [ThunkMiddlewareFor<S, {}, A>]
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {
const curriedGetDefaultMiddleware = curryGetDefaultMiddleware<S>()

Expand Down
40 changes: 25 additions & 15 deletions src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { NoInfer } from './tsHelpers'
*/
export type SliceActionCreator<P> = PayloadActionCreator<P>

export type ActionsOfCaseReducersActions<T extends CaseReducerActions<SliceCaseReducers<any>>> = {
[Type in keyof T]: ReturnType<T[Type]>
}[keyof T]

/**
* The return value of `createSlice`
*
Expand All @@ -41,13 +45,13 @@ export interface Slice<
/**
* The slice's reducer.
*/
reducer: Reducer<State>
reducer: Reducer<State, ActionsOfCaseReducersActions<CaseReducerActions<CaseReducers, Name>>>

/**
* Action creators for the types of actions that are handled by the slice
* reducer.
*/
actions: CaseReducerActions<CaseReducers>
actions: CaseReducerActions<CaseReducers, Name>

/**
* The individual case reducer functions that were passed in the `reducers` parameter.
Expand Down Expand Up @@ -159,10 +163,15 @@ export type SliceCaseReducers<State> = {
*
* @public
*/
export type CaseReducerActions<CaseReducers extends SliceCaseReducers<any>> = {
[Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type]>
: ActionCreatorForCaseReducer<CaseReducers[Type]>
export type CaseReducerActions<
CaseReducers extends SliceCaseReducers<any>,
SliceName extends string = string
> = {
[Type in string & keyof CaseReducers]: CaseReducers[Type] extends {
prepare: any
}
? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type], `${SliceName}/${Type}`>
: ActionCreatorForCaseReducer<CaseReducers[Type], `${SliceName}/${Type}`>
}

/**
Expand All @@ -171,22 +180,23 @@ export type CaseReducerActions<CaseReducers extends SliceCaseReducers<any>> = {
* @internal
*/
type ActionCreatorForCaseReducerWithPrepare<
CR extends { prepare: any }
> = _ActionCreatorWithPreparedPayload<CR['prepare'], string>
CR extends { prepare: any },
Type extends string = string
> = _ActionCreatorWithPreparedPayload<CR['prepare'], Type>

/**
* Get a `PayloadActionCreator` type for a passed `CaseReducer`
*
* @internal
*/
type ActionCreatorForCaseReducer<CR> = CR extends (
state: any,
action: infer Action
) => any
type ActionCreatorForCaseReducer<
CR,
Type extends string = string
> = CR extends (state: any, action: infer Action) => any
? Action extends { payload: infer P }
? PayloadActionCreator<P>
: ActionCreatorWithoutPayload
: ActionCreatorWithoutPayload
? PayloadActionCreator<P, Type>
: ActionCreatorWithoutPayload<Type>
: ActionCreatorWithoutPayload<Type>

/**
* Extracts the CaseReducers out of a `reducers` object, even if they are
Expand Down
9 changes: 5 additions & 4 deletions src/getDefaultMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ interface GetDefaultMiddlewareOptions {

export type ThunkMiddlewareFor<
S,
O extends GetDefaultMiddlewareOptions = {}
O extends GetDefaultMiddlewareOptions = {},
A extends AnyAction = AnyAction
> = O extends {
thunk: false
}
? never
: O extends { thunk: { extraArgument: infer E } }
? ThunkMiddleware<S, AnyAction, E>
? ThunkMiddleware<S, A, E>
:
| ThunkMiddleware<S, AnyAction, null> //The ThunkMiddleware with a `null` ExtraArgument is here to provide backwards-compatibility.
| ThunkMiddleware<S, AnyAction>
| ThunkMiddleware<S, A, null> //The ThunkMiddleware with a `null` ExtraArgument is here to provide backwards-compatibility.
| ThunkMiddleware<S, A>
Comment on lines +32 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason for this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because the reducer has an intersection type with ThunkMiddlewareFor, so without it, the reducer will merge the action argument to AnyAction.

Copy link
Author

@gtkatakura gtkatakura Apr 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ThunkMiddlewareFor is passed to configureStore and is merged into reducer here:

export function configureStore<
  S = any,
  A extends Action = AnyAction,
  M extends Middlewares<S> = [ThunkMiddlewareFor<S, {}, A>]
>(options: ConfigureStoreOptions<S, A, M>): EnhancedStore<S, A, M> {


export interface EnhancedStore<
  S = any,
  A extends Action = AnyAction,
  M extends Middlewares<S> = Middlewares<S>
> extends Store<S, A> {
  /**
   * The `dispatch` method of your store, enhanced by all its middlewares.
   *
   * @inheritdoc
   */
  dispatch: DispatchForMiddlewares<M> & Dispatch<A>
}


export type CurriedGetDefaultMiddleware<S = any> = <
O extends Partial<GetDefaultMiddlewareOptions> = {
Expand Down
58 changes: 51 additions & 7 deletions type-tests/files/createSlice.typetest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, AnyAction, Reducer } from 'redux'
import { Action, AnyAction, combineReducers, Reducer } from 'redux'
import {
ActionCreatorWithNonInferrablePayload,
ActionCreatorWithOptionalPayload,
Expand Down Expand Up @@ -68,8 +68,12 @@ const value = actionCreators.anyKey

/* Reducer */

const reducer: Reducer<number, PayloadAction> = slice.reducer
type InferredTypeActions = 'counter/increment' | 'counter/decrement'

const reducer: Reducer<number, PayloadAction<void, InferredTypeActions>> = slice.reducer

// @ts-expect-error
const stringTypeActionsReducer: Reducer<number, PayloadAction> = slice.reducer
// @ts-expect-error
const stringReducer: Reducer<string, PayloadAction> = slice.reducer
// @ts-expect-error
Expand All @@ -84,6 +88,40 @@ const value = actionCreators.anyKey
slice.actions.other(1)
}

/*
* Test: createSlice() safetly type actions with combineReducers
*/
{
const firstAction = createAction<{ count: number }>('FIRST_ACTION')

const slice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state: number, action) => state + action.payload,
decrement: (state: number, action) => state - action.payload
},
extraReducers: {
[firstAction.type]: (state: number, action) =>
state + action.payload.count
}
})

/* Reducer */

const reducer = combineReducers({
counter: slice.reducer,
})

reducer({ counter: 0 }, { type: 'counter/decrement', payload: undefined })
reducer({ counter: 0 }, { type: 'counter/increment', payload: undefined })

// @ts-expect-error
reducer({ counter: 0 }, { type: 'decrement', payload: undefined })
// @ts-expect-error
reducer({ counter: 0 }, { type: 'any-string', payload: undefined })
}

/*
* Test: Slice action creator types are inferred.
*/
Expand Down Expand Up @@ -139,7 +177,7 @@ const value = actionCreators.anyKey
}

/*
* Test: Slice action creator types properties are "string"
* Test: Slice action creator types properties are `${sliceName}/${reducerPropertyName}`
*/
{
const counter = createSlice({
Expand All @@ -159,10 +197,16 @@ const value = actionCreators.anyKey
const t: string = counter.actions.decrement.type
const u: string = counter.actions.multiply.type

const a: 'counter/increment' = counter.actions.increment.type
const b: 'counter/decrement' = counter.actions.decrement.type
const c: 'counter/multiply' = counter.actions.multiply.type

// @ts-expect-error
const x: 'increment' = counter.actions.increment.type
// @ts-expect-error
const x: 'counter/increment' = counter.actions.increment.type
const y: 'decrement' = counter.actions.decrement.type
// @ts-expect-error
const y: 'increment' = counter.actions.increment.type
const z: 'multiply' = counter.actions.multiply.type
}

/*
Expand Down Expand Up @@ -193,7 +237,7 @@ const value = actionCreators.anyKey
}
})

expectType<string>(counter.actions.incrementByStrLen('test').type)
expectType<'test/incrementByStrLen'>(counter.actions.incrementByStrLen('test').type)
expectType<number>(counter.actions.incrementByStrLen('test').payload)
expectType<string>(counter.actions.concatMetaStrLen('test').payload)
expectType<number>(counter.actions.concatMetaStrLen('test').meta)
Expand Down Expand Up @@ -272,7 +316,7 @@ const value = actionCreators.anyKey
name: 'counter',
initialState: 0,
reducers: {
increment(state, action: PayloadAction<number>) {
increment(state, action) {
return state + action.payload
},
decrement: {
Expand Down