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

Prefer use of Action or UnknownAction instead of AnyAction #4520

Merged
merged 12 commits into from
May 30, 2023
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ coverage
dist
lib
es
types

# Yarn
.cache
Expand Down
14 changes: 7 additions & 7 deletions docs/usage/UsageWithTypescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@ You could add this to your ESLint config as an example:

[Reducers](../tutorials/fundamentals/part-3-state-actions-reducers.md) are pure functions that receive the current `state` and incoming `action` as arguments, and return a new state.

If you are using Redux Toolkit's `createSlice`, you should rarely need to specifically type a reducer separately. If you do actually write a standalone reducer, it's typically sufficient to declare the type of the `initialState` value, and type the `action` as `AnyAction`:
If you are using Redux Toolkit's `createSlice`, you should rarely need to specifically type a reducer separately. If you do actually write a standalone reducer, it's typically sufficient to declare the type of the `initialState` value, and type the `action` as `UnknownAction`:

```ts
import { AnyAction } from 'redux'
import { UnknownAction } from 'redux'

interface CounterState {
value: number
Expand All @@ -222,7 +222,7 @@ const initialState: CounterState = {

export default function counterReducer(
state = initialState,
action: AnyAction
action: UnknownAction
) {
// logic here
}
Expand Down Expand Up @@ -297,16 +297,16 @@ export type ThunkAction<
> = (dispatch: ThunkDispatch<S, E, A>, getState: () => S, extraArgument: E) => R
```

You will typically want to provide the `R` (return type) and `S` (state) generic arguments. Unfortunately, TS does not allow only providing _some_ generic arguments, so the usual values for the other arguments are `unknown` for `E` and `AnyAction` for `A`:
You will typically want to provide the `R` (return type) and `S` (state) generic arguments. Unfortunately, TS does not allow only providing _some_ generic arguments, so the usual values for the other arguments are `unknown` for `E` and `UnknownAction` for `A`:

```ts
import { AnyAction } from 'redux'
import { UnknownAction } from 'redux'
import { sendMessage } from './store/chat/actions'
import { RootState } from './store'
import { ThunkAction } from 'redux-thunk'

export const thunkSendMessage =
(message: string): ThunkAction<void, RootState, unknown, AnyAction> =>
(message: string): ThunkAction<void, RootState, unknown, UnknownAction> =>
async dispatch => {
const asyncResp = await exampleAPI()
dispatch(
Expand All @@ -330,7 +330,7 @@ export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
AnyAction
UnknownAction
>
```

Expand Down
2 changes: 1 addition & 1 deletion docs/usage/migrating-to-modern-redux.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ const store = configureStore({
// Inferred state type: {todos: TodosState, counter: CounterState}
export type RootState = ReturnType<typeof store.getState>

// Inferred dispatch type: Dispatch & ThunkDispatch<RootState, undefined, AnyAction>
// Inferred dispatch type: Dispatch & ThunkDispatch<RootState, undefined, UnknownAction>
export type AppDispatch = typeof store.dispatch
// highlight-end
```
Expand Down
10 changes: 3 additions & 7 deletions src/bindActionCreators.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { Dispatch } from './types/store'
import {
AnyAction,
ActionCreator,
ActionCreatorsMapObject
} from './types/actions'
import { ActionCreator, ActionCreatorsMapObject, Action } from './types/actions'
import { kindOf } from './utils/kindOf'

function bindActionCreator<A extends AnyAction = AnyAction>(
function bindActionCreator<A extends Action>(
actionCreator: ActionCreator<A>,
dispatch: Dispatch
dispatch: Dispatch<A>
) {
return function (this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
Expand Down
4 changes: 2 additions & 2 deletions src/combineReducers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyAction, Action } from './types/actions'
import { Action } from './types/actions'
import {
ActionFromReducersMapObject,
PreloadedStateShapeFromReducersMapObject,
Expand Down Expand Up @@ -156,7 +156,7 @@ export default function combineReducers(reducers: {

return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: AnyAction
action: Action
) {
if (shapeAssertionError) {
throw shapeAssertionError
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export { ActionCreator, ActionCreatorsMapObject } from './types/actions'
// middleware
export { MiddlewareAPI, Middleware } from './types/middleware'
// actions
export { Action, AnyAction } from './types/actions'
export { Action, UnknownAction, AnyAction } from './types/actions'

export {
createStore,
Expand Down
16 changes: 15 additions & 1 deletion src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
*
* @template T the type of the action's `type` tag.
*/
export interface Action<T extends string = string> {
// this needs to be a type, not an interface
// /~https://github.com/microsoft/TypeScript/issues/15300
export type Action<T extends string = string> = {
type: T
}

Expand All @@ -24,6 +26,18 @@ export interface Action<T extends string = string> {
* This is not part of `Action` itself to prevent types that extend `Action` from
* having an index signature.
*/
export interface UnknownAction extends Action {
// Allows any extra properties to be defined in an action.
[extraProps: string]: unknown
}

/**
* An Action type which accepts any other properties.
* This is mainly for the use of the `Reducer` type.
* This is not part of `Action` itself to prevent types that extend `Action` from
* having an index signature.
* @deprecated use Action or UnknownAction instead
*/
export interface AnyAction extends Action {
// Allows any extra properties to be defined in an action.
[extraProps: string]: any
Expand Down
14 changes: 6 additions & 8 deletions src/types/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, AnyAction } from './actions'
import { Action, UnknownAction } from './actions'

/* reducers */

Expand Down Expand Up @@ -29,7 +29,7 @@ import { Action, AnyAction } from './actions'
*/
export type Reducer<
S = any,
A extends Action = AnyAction,
A extends Action = UnknownAction,
PreloadedState = S
> = (state: S | PreloadedState | undefined, action: A) => S

Expand All @@ -42,7 +42,7 @@ export type Reducer<
*/
export type ReducersMapObject<
S = any,
A extends Action = AnyAction,
A extends Action = UnknownAction,
PreloadedState = S
> = keyof PreloadedState extends keyof S
? {
Expand All @@ -63,7 +63,7 @@ export type StateFromReducersMapObject<M> = M[keyof M] extends
| Reducer<any, any, any>
| undefined
? {
[P in keyof M]: M[P] extends Reducer<infer S> ? S : never
[P in keyof M]: M[P] extends Reducer<infer S, any, any> ? S : never
}
: never

Expand All @@ -83,9 +83,7 @@ export type ReducerFromReducersMapObject<M> = M[keyof M] extends
*
* @template R Type of reducer.
*/
export type ActionFromReducer<R> = R extends
| Reducer<any, infer A, any>
| undefined
export type ActionFromReducer<R> = R extends Reducer<any, infer A, any>
? A
: never

Expand All @@ -109,7 +107,7 @@ export type PreloadedStateShapeFromReducersMapObject<M> = M[keyof M] extends
? {
[P in keyof M]: M[P] extends (
inputState: infer InputState,
action: AnyAction
action: UnknownAction
) => any
? InputState
: never
Expand Down
6 changes: 3 additions & 3 deletions src/types/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, AnyAction } from './actions'
import { Action, UnknownAction } from './actions'
import { Reducer } from './reducers'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import _$$observable from '../utils/symbol-observable'
Expand All @@ -24,7 +24,7 @@ import _$$observable from '../utils/symbol-observable'
* @template A The type of things (actions or otherwise) which may be
* dispatched.
*/
export interface Dispatch<A extends Action = AnyAction> {
export interface Dispatch<A extends Action = UnknownAction> {
<T extends A>(action: T, ...extraArgs: any[]): T
}

Expand Down Expand Up @@ -80,7 +80,7 @@ export type Observer<T> = {
*/
export interface Store<
S = any,
A extends Action = AnyAction,
A extends Action = UnknownAction,
StateExt extends {} = {}
> {
/**
Expand Down
3 changes: 1 addition & 2 deletions test/applyMiddleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
applyMiddleware,
Middleware,
MiddlewareAPI,
AnyAction,
Action,
Store,
Dispatch
Expand Down Expand Up @@ -128,7 +127,7 @@ describe('applyMiddleware', () => {
const spy = vi.fn()
const testCallArgs = ['test']

interface MultiDispatch<A extends Action = AnyAction> {
interface MultiDispatch<A extends Action = Action> {
<T extends A>(action: T, extraArg?: string[]): T
}

Expand Down
17 changes: 8 additions & 9 deletions test/combineReducers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
combineReducers,
Reducer,
Action,
AnyAction,
__DO_NOT_USE__ActionTypes as ActionTypes
} from 'redux'
import { vi } from 'vitest'
Expand Down Expand Up @@ -88,7 +87,7 @@ describe('Utils', () => {
expect(() => reducer({ counter: 0 }, null)).toThrow(
/"counter".*an action/
)
expect(() => reducer({ counter: 0 }, {} as unknown as AnyAction)).toThrow(
expect(() => reducer({ counter: 0 }, {} as unknown as Action)).toThrow(
/"counter".*an action/
)
})
Expand Down Expand Up @@ -117,9 +116,9 @@ describe('Utils', () => {
throw new Error('Error thrown in reducer')
}
})
expect(() =>
reducer(undefined, undefined as unknown as AnyAction)
).toThrow(/Error thrown in reducer/)
expect(() => reducer(undefined, undefined as unknown as Action)).toThrow(
/Error thrown in reducer/
)
})

it('maintains referential equality if the reducers it is combining do', () => {
Expand Down Expand Up @@ -179,9 +178,9 @@ describe('Utils', () => {
}
}
})
expect(() =>
reducer(undefined, undefined as unknown as AnyAction)
).toThrow(/"counter".*private/)
expect(() => reducer(undefined, undefined as unknown as Action)).toThrow(
/"counter".*private/
)
})

it('warns if no reducers are passed to combineReducers', () => {
Expand All @@ -203,7 +202,7 @@ describe('Utils', () => {
it('warns if input state does not match reducer shape', () => {
const preSpy = console.error
const spy = vi.fn()
const nullAction = undefined as unknown as AnyAction
const nullAction = undefined as unknown as Action
console.error = spy

interface ShapeState {
Expand Down
Loading