Skip to content

Commit

Permalink
Merge pull request #4520 from EskiMojo14/unknown-action
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson authored May 30, 2023
2 parents 6dddd24 + 61515da commit 5a9d19b
Show file tree
Hide file tree
Showing 17 changed files with 135 additions and 104 deletions.
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

0 comments on commit 5a9d19b

Please sign in to comment.