From 456cc345d8ffdde5d798f8509909eb58357ae23c Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 16 Apr 2023 01:21:25 +0100 Subject: [PATCH 1/8] Encourage use of Action or UnknownAction instead of AnyAction --- .gitignore | 1 - docs/usage/UsageWithTypescript.md | 14 +++++----- docs/usage/migrating-to-modern-redux.mdx | 2 +- src/bindActionCreators.ts | 8 ++---- src/combineReducers.ts | 4 +-- src/index.ts | 2 +- src/types/actions.ts | 14 +++++++++- src/types/reducers.ts | 10 +++---- test/applyMiddleware.spec.ts | 3 +-- test/combineReducers.spec.ts | 19 +++++++------ test/createStore.spec.ts | 34 ++++++++++++++++++++++-- test/helpers/actionCreators.ts | 29 ++++++++++---------- test/helpers/reducers.ts | 26 +++++++----------- test/typescript/enhancers.ts | 10 +++---- test/typescript/middleware.ts | 3 +-- test/typescript/reducers.ts | 21 ++++++++++----- 16 files changed, 118 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 7c78e19788..49260393b8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ coverage dist lib es -types # Yarn .cache diff --git a/docs/usage/UsageWithTypescript.md b/docs/usage/UsageWithTypescript.md index b702012072..3984a89cff 100644 --- a/docs/usage/UsageWithTypescript.md +++ b/docs/usage/UsageWithTypescript.md @@ -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 @@ -222,7 +222,7 @@ const initialState: CounterState = { export default function counterReducer( state = initialState, - action: AnyAction + action: UnknownAction ) { // logic here } @@ -297,16 +297,16 @@ export type ThunkAction< > = (dispatch: ThunkDispatch, 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 => + (message: string): ThunkAction => async dispatch => { const asyncResp = await exampleAPI() dispatch( @@ -330,7 +330,7 @@ export type AppThunk = ThunkAction< ReturnType, RootState, unknown, - AnyAction + UnknownAction > ``` diff --git a/docs/usage/migrating-to-modern-redux.mdx b/docs/usage/migrating-to-modern-redux.mdx index 2c6e801333..402cdc9b76 100644 --- a/docs/usage/migrating-to-modern-redux.mdx +++ b/docs/usage/migrating-to-modern-redux.mdx @@ -846,7 +846,7 @@ const store = configureStore({ // Inferred state type: {todos: TodosState, counter: CounterState} export type RootState = ReturnType -// Inferred dispatch type: Dispatch & ThunkDispatch +// Inferred dispatch type: Dispatch & ThunkDispatch export type AppDispatch = typeof store.dispatch // highlight-end ``` diff --git a/src/bindActionCreators.ts b/src/bindActionCreators.ts index 022d15bb0e..80b4e7c5ac 100644 --- a/src/bindActionCreators.ts +++ b/src/bindActionCreators.ts @@ -1,12 +1,8 @@ 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( +function bindActionCreator( actionCreator: ActionCreator, dispatch: Dispatch ) { diff --git a/src/combineReducers.ts b/src/combineReducers.ts index 2894f97c4e..0a3c0b65b4 100644 --- a/src/combineReducers.ts +++ b/src/combineReducers.ts @@ -1,4 +1,4 @@ -import { AnyAction, Action } from './types/actions' +import { Action } from './types/actions' import { ActionFromReducersMapObject, PreloadedStateShapeFromReducersMapObject, @@ -156,7 +156,7 @@ export default function combineReducers(reducers: { return function combination( state: StateFromReducersMapObject = {}, - action: AnyAction + action: Action ) { if (shapeAssertionError) { throw shapeAssertionError diff --git a/src/index.ts b/src/index.ts index ef3a1791ed..8bdd6c4e7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/types/actions.ts b/src/types/actions.ts index 7446886922..e1bdea4368 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -15,7 +15,7 @@ * * @template T the type of the action's `type` tag. */ -export interface Action { +export interface Action { type: T } @@ -25,6 +25,18 @@ export interface Action { * 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 diff --git a/src/types/reducers.ts b/src/types/reducers.ts index 7fd31b3c49..dfd6a1d2bc 100644 --- a/src/types/reducers.ts +++ b/src/types/reducers.ts @@ -1,4 +1,4 @@ -import { Action, AnyAction } from './actions' +import { Action, UnknownAction } from './actions' /* reducers */ @@ -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 @@ -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 ? { @@ -63,7 +63,7 @@ export type StateFromReducersMapObject = M[keyof M] extends | Reducer | undefined ? { - [P in keyof M]: M[P] extends Reducer ? S : never + [P in keyof M]: M[P] extends Reducer ? S : never } : never @@ -109,7 +109,7 @@ export type PreloadedStateShapeFromReducersMapObject = M[keyof M] extends ? { [P in keyof M]: M[P] extends ( inputState: infer InputState, - action: AnyAction + action: UnknownAction ) => any ? InputState : never diff --git a/test/applyMiddleware.spec.ts b/test/applyMiddleware.spec.ts index 45be9c4fa7..1e4eea6923 100644 --- a/test/applyMiddleware.spec.ts +++ b/test/applyMiddleware.spec.ts @@ -3,7 +3,6 @@ import { applyMiddleware, Middleware, MiddlewareAPI, - AnyAction, Action, Store, Dispatch @@ -128,7 +127,7 @@ describe('applyMiddleware', () => { const spy = vi.fn() const testCallArgs = ['test'] - interface MultiDispatch { + interface MultiDispatch { (action: T, extraArg?: string[]): T } diff --git a/test/combineReducers.spec.ts b/test/combineReducers.spec.ts index 356e72b52c..df2dfdd470 100644 --- a/test/combineReducers.spec.ts +++ b/test/combineReducers.spec.ts @@ -4,7 +4,6 @@ import { combineReducers, Reducer, Action, - AnyAction, __DO_NOT_USE__ActionTypes as ActionTypes } from 'redux' import { vi } from 'vitest' @@ -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/ ) }) @@ -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('allows a symbol to be used as an action type', () => { @@ -185,7 +184,7 @@ describe('Utils', () => { it('throws an error on first call if a reducer attempts to handle a private action', () => { const reducer = combineReducers({ - counter(state: number, action: Action) { + counter(state: number, action: Action) { switch (action.type) { case 'increment': return state + 1 @@ -199,9 +198,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', () => { @@ -223,7 +222,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 { diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index 6259461d2b..46ac4fab4c 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -66,6 +66,7 @@ describe('createStore', () => { const store = createStore(reducers.todos) expect(store.getState()).toEqual([]) + // @ts-expect-error store.dispatch(unknownAction()) expect(store.getState()).toEqual([]) @@ -104,6 +105,7 @@ describe('createStore', () => { } ]) + // @ts-expect-error store.dispatch(unknownAction()) expect(store.getState()).toEqual([ { @@ -211,10 +213,12 @@ describe('createStore', () => { const listenerB = vi.fn() let unsubscribeA = store.subscribe(listenerA) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(1) expect(listenerB.mock.calls.length).toBe(0) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(2) expect(listenerB.mock.calls.length).toBe(0) @@ -223,6 +227,7 @@ describe('createStore', () => { expect(listenerA.mock.calls.length).toBe(2) expect(listenerB.mock.calls.length).toBe(0) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(1) @@ -231,6 +236,7 @@ describe('createStore', () => { expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(1) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(2) @@ -239,6 +245,7 @@ describe('createStore', () => { expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(2) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(2) @@ -247,6 +254,7 @@ describe('createStore', () => { expect(listenerA.mock.calls.length).toBe(3) expect(listenerB.mock.calls.length).toBe(2) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(4) expect(listenerB.mock.calls.length).toBe(2) @@ -263,6 +271,7 @@ describe('createStore', () => { unsubscribeA() unsubscribeA() + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(0) expect(listenerB.mock.calls.length).toBe(1) @@ -278,6 +287,7 @@ describe('createStore', () => { unsubscribeSecond() unsubscribeSecond() + // @ts-expect-error store.dispatch(unknownAction()) expect(listener.mock.calls.length).toBe(1) }) @@ -295,7 +305,9 @@ describe('createStore', () => { }) store.subscribe(listenerC) + // @ts-expect-error store.dispatch(unknownAction()) + // @ts-expect-error store.dispatch(unknownAction()) expect(listenerA.mock.calls.length).toBe(2) @@ -323,11 +335,13 @@ describe('createStore', () => { ) unsubscribeHandles.push(store.subscribe(() => listener3())) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) expect(listener2.mock.calls.length).toBe(1) expect(listener3.mock.calls.length).toBe(1) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) expect(listener2.mock.calls.length).toBe(1) @@ -355,11 +369,13 @@ describe('createStore', () => { maybeAddThirdListener() }) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) expect(listener2.mock.calls.length).toBe(1) expect(listener3.mock.calls.length).toBe(0) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(2) expect(listener2.mock.calls.length).toBe(2) @@ -384,6 +400,7 @@ describe('createStore', () => { unsubscribe1() unsubscribe4 = store.subscribe(listener4) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) @@ -394,6 +411,7 @@ describe('createStore', () => { store.subscribe(listener2) store.subscribe(listener3) + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) expect(listener2.mock.calls.length).toBe(2) @@ -401,6 +419,7 @@ describe('createStore', () => { expect(listener4.mock.calls.length).toBe(1) unsubscribe4() + // @ts-expect-error store.dispatch(unknownAction()) expect(listener1.mock.calls.length).toBe(1) expect(listener2.mock.calls.length).toBe(3) @@ -447,6 +466,7 @@ describe('createStore', () => { it('only accepts plain object actions', () => { const store = createStore(reducers.todos) + // @ts-expect-error expect(() => store.dispatch(unknownAction())).not.toThrow() function AwesomeMap() {} @@ -486,6 +506,7 @@ describe('createStore', () => { expect(() => store.dispatch( + // @ts-expect-error dispatchInMiddle(store.dispatch.bind(store, unknownAction())) ) ).toThrow(/may not dispatch/) @@ -528,6 +549,7 @@ describe('createStore', () => { const store = createStore(reducers.errorThrowingReducer) expect(() => store.dispatch(throwError())).toThrow() + // @ts-expect-error expect(() => store.dispatch(unknownAction())).not.toThrow() }) @@ -567,6 +589,7 @@ describe('createStore', () => { it('throws if action type is undefined', () => { const store = createStore(reducers.todos) + // @ts-expect-error expect(() => store.dispatch({ type: undefined })).toThrow( /Actions may not have an undefined "type" property/ ) @@ -574,9 +597,13 @@ describe('createStore', () => { it('does not throw if action type is falsy', () => { const store = createStore(reducers.todos) + // @ts-expect-error expect(() => store.dispatch({ type: false })).not.toThrow() + // @ts-expect-error expect(() => store.dispatch({ type: 0 })).not.toThrow() + // @ts-expect-error expect(() => store.dispatch({ type: null })).not.toThrow() + // @ts-expect-error expect(() => store.dispatch({ type: '' })).not.toThrow() }) @@ -655,12 +682,14 @@ describe('createStore', () => { expect(() => createStore(reducers.todos, undefined, x => x)).not.toThrow() - expect(() => createStore(reducers.todos, x => x)).not.toThrow() + expect(() => + createStore(reducers.todos, x => x) + ).not.toThrow() expect(() => createStore(reducers.todos, [])).not.toThrow() expect(() => - createStore(reducers.todos, {}) + createStore(reducers.todos, {}) ).not.toThrow() }) @@ -863,6 +892,7 @@ describe('createStore', () => { ) store.replaceReducer( + // @ts-expect-error combineReducers({ y: combineReducers({ z: reducer diff --git a/test/helpers/actionCreators.ts b/test/helpers/actionCreators.ts index d7a5a57e33..92f992a0ee 100644 --- a/test/helpers/actionCreators.ts +++ b/test/helpers/actionCreators.ts @@ -7,9 +7,10 @@ import { THROW_ERROR, UNKNOWN_ACTION } from './actionTypes' -import { Action, AnyAction, Dispatch } from 'redux' +import { TodoAction } from './reducers' +import { Dispatch } from 'redux' -export function addTodo(text: string): AnyAction { +export function addTodo(text: string): TodoAction { return { type: ADD_TODO, text } } @@ -31,42 +32,42 @@ export function addTodoIfEmpty(text: string) { } } -export function dispatchInMiddle(boundDispatchFn: () => void): AnyAction { +export function dispatchInMiddle(boundDispatchFn: () => void) { return { type: DISPATCH_IN_MIDDLE, boundDispatchFn - } + } as const } -export function getStateInMiddle(boundGetStateFn: () => void): AnyAction { +export function getStateInMiddle(boundGetStateFn: () => void) { return { type: GET_STATE_IN_MIDDLE, boundGetStateFn - } + } as const } -export function subscribeInMiddle(boundSubscribeFn: () => void): AnyAction { +export function subscribeInMiddle(boundSubscribeFn: () => void) { return { type: SUBSCRIBE_IN_MIDDLE, boundSubscribeFn - } + } as const } -export function unsubscribeInMiddle(boundUnsubscribeFn: () => void): AnyAction { +export function unsubscribeInMiddle(boundUnsubscribeFn: () => void) { return { type: UNSUBSCRIBE_IN_MIDDLE, boundUnsubscribeFn - } + } as const } -export function throwError(): Action { +export function throwError() { return { type: THROW_ERROR - } + } as const } -export function unknownAction(): Action { +export function unknownAction() { return { type: UNKNOWN_ACTION - } + } as const } diff --git a/test/helpers/reducers.ts b/test/helpers/reducers.ts index d676a75a8a..5ea9d158af 100644 --- a/test/helpers/reducers.ts +++ b/test/helpers/reducers.ts @@ -6,7 +6,6 @@ import { UNSUBSCRIBE_IN_MIDDLE, THROW_ERROR } from './actionTypes' -import { AnyAction } from 'redux' function id(state: { id: number }[]) { return ( @@ -18,9 +17,9 @@ export interface Todo { id: number text: string } -export type TodoAction = { type: 'ADD_TODO'; text: string } | AnyAction +export type TodoAction = { type: 'ADD_TODO'; text: string } -export function todos(state: Todo[] = [], action: TodoAction) { +export function todos(state: Todo[] = [], action: TodoAction): Todo[] { switch (action.type) { case ADD_TODO: return [ @@ -52,9 +51,7 @@ export function todosReverse(state: Todo[] = [], action: TodoAction) { export function dispatchInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundDispatchFn: () => void } - | AnyAction + action: { type: 'DISPATCH_IN_MIDDLE'; boundDispatchFn: () => void } ) { switch (action.type) { case DISPATCH_IN_MIDDLE: @@ -67,9 +64,7 @@ export function dispatchInTheMiddleOfReducer( export function getStateInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundGetStateFn: () => void } - | AnyAction + action: { type: 'GET_STATE_IN_MIDDLE'; boundGetStateFn: () => void } ) { switch (action.type) { case GET_STATE_IN_MIDDLE: @@ -82,9 +77,7 @@ export function getStateInTheMiddleOfReducer( export function subscribeInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundSubscribeFn: () => void } - | AnyAction + action: { type: 'SUBSCRIBE_IN_MIDDLE'; boundSubscribeFn: () => void } ) { switch (action.type) { case SUBSCRIBE_IN_MIDDLE: @@ -97,9 +90,7 @@ export function subscribeInTheMiddleOfReducer( export function unsubscribeInTheMiddleOfReducer( state = [], - action: - | { type: 'DISPATCH_IN_MIDDLE'; boundUnsubscribeFn: () => void } - | AnyAction + action: { type: 'UNSUBSCRIBE_IN_MIDDLE'; boundUnsubscribeFn: () => void } ) { switch (action.type) { case UNSUBSCRIBE_IN_MIDDLE: @@ -110,7 +101,10 @@ export function unsubscribeInTheMiddleOfReducer( } } -export function errorThrowingReducer(state = [], action: AnyAction) { +export function errorThrowingReducer( + state = [], + action: { type: 'THROW_ERROR' } +) { switch (action.type) { case THROW_ERROR: throw new Error() diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 9771d5ad68..098ba591c8 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -1,4 +1,4 @@ -import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from 'redux' +import { StoreEnhancer, Action, Reducer, createStore } from 'redux' interface State { someField: 'string' @@ -169,8 +169,7 @@ function replaceReducerExtender() { ExtraState >(initialReducer, enhancer) - const newReducer = (state: PartialState = { test: true }, _: AnyAction) => - state + const newReducer = (state: PartialState = { test: true }, _: Action) => state store.replaceReducer(newReducer) store.getState().test @@ -260,7 +259,7 @@ function mhelmersonExample() { store.getState().wrongField store.getState().test - const newReducer = (state: PartialState = { test: true }, _: AnyAction) => + const newReducer = (state: PartialState = { test: true }, _: Action) => state store.replaceReducer(newReducer) @@ -335,8 +334,7 @@ function finalHelmersonExample() { // @ts-expect-error store.getState().wrongField - const newReducer = (state: PartialState = { test: true }, _: AnyAction) => - state + const newReducer = (state: PartialState = { test: true }, _: Action) => state store.replaceReducer(newReducer) store.getState().test diff --git a/test/typescript/middleware.ts b/test/typescript/middleware.ts index fa9985261f..026238d634 100644 --- a/test/typescript/middleware.ts +++ b/test/typescript/middleware.ts @@ -5,8 +5,7 @@ import { createStore, Dispatch, Reducer, - Action, - AnyAction + Action } from 'redux' /** diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index 90a3b7742c..b2e7d37c7d 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -1,4 +1,10 @@ -import { Reducer, Action, combineReducers, ReducersMapObject } from 'redux' +import { + Reducer, + Action, + combineReducers, + ReducersMapObject, + AnyAction +} from 'redux' /** * Simple reducer definition with no action shape checks. @@ -13,14 +19,16 @@ function simple() { const reducer: Reducer = (state = 0, action) => { if (action.type === 'INCREMENT') { const { count = 1 } = action - - return state + count + if (typeof count === 'number') { + return state + count + } } if (action.type === 'DECREMENT') { const { count = 1 } = action - - return state - count + if (typeof count === 'number') { + return state + count + } } return state @@ -186,8 +194,9 @@ function typeGuards() { count?: number } - const reducer: Reducer = (state = 0, action) => { + const reducer: Reducer = (state = 0, action) => { if (isAction(action, 'INCREMENT')) { + // TODO: this doesn't seem to work correctly with UnknownAction - `action` becomes `UnknownAction & IncrementAction` // Action shape is determined by the type guard returned from `isAction` // @ts-expect-error action.wrongField From 985fdefdeb1bc5cf09b5dbd81f291bfec101e296 Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 16 Apr 2023 14:42:21 +0100 Subject: [PATCH 2/8] make Action a type --- src/types/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/actions.ts b/src/types/actions.ts index e1bdea4368..d10f2637c6 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -15,7 +15,7 @@ * * @template T the type of the action's `type` tag. */ -export interface Action { +export type Action = { type: T } From fe3d667990d0f0b4cb63c077f8d8b9d7a2e1a3cd Mon Sep 17 00:00:00 2001 From: Ben Durrant Date: Sun, 16 Apr 2023 15:12:21 +0100 Subject: [PATCH 3/8] default dispatch to unknownaction --- src/types/store.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/store.ts b/src/types/store.ts index e33d783005..30c8c8b366 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -1,4 +1,4 @@ -import { Action, AnyAction } from './actions' +import { Action, UnknownAction } from './actions' import { Reducer } from './reducers' import '../utils/symbol-observable' @@ -23,7 +23,7 @@ import '../utils/symbol-observable' * @template A The type of things (actions or otherwise) which may be * dispatched. */ -export interface Dispatch { +export interface Dispatch { (action: T, ...extraArgs: any[]): T } @@ -79,7 +79,7 @@ export type Observer = { */ export interface Store< S = any, - A extends Action = AnyAction, + A extends Action = UnknownAction, StateExt extends {} = {} > { /** From 043da889735891d1e07092be4aedc19de8a90ed5 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Fri, 5 May 2023 19:55:27 +0100 Subject: [PATCH 4/8] rm unused expect error --- test/createStore.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/createStore.spec.ts b/test/createStore.spec.ts index 46ac4fab4c..c54024ef4e 100644 --- a/test/createStore.spec.ts +++ b/test/createStore.spec.ts @@ -892,7 +892,6 @@ describe('createStore', () => { ) store.replaceReducer( - // @ts-expect-error combineReducers({ y: combineReducers({ z: reducer From 687e2fe771c71d9e05f5395041ee5b734cc8c884 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 16 May 2023 11:36:44 +0100 Subject: [PATCH 5/8] "fix" type --- src/bindActionCreators.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindActionCreators.ts b/src/bindActionCreators.ts index 80b4e7c5ac..3d63d7fe18 100644 --- a/src/bindActionCreators.ts +++ b/src/bindActionCreators.ts @@ -4,7 +4,7 @@ import { kindOf } from './utils/kindOf' function bindActionCreator( actionCreator: ActionCreator, - dispatch: Dispatch + dispatch: Dispatch ) { return function (this: any, ...args: any[]) { return dispatch(actionCreator.apply(this, args)) From 20631993c1c68ce398e9b0d8a9326f17db042c6f Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 16 May 2023 13:16:06 +0100 Subject: [PATCH 6/8] action needs to be a type not interface --- src/types/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/actions.ts b/src/types/actions.ts index a1ced84c97..16106e2e54 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -14,7 +14,7 @@ * * @template T the type of the action's `type` tag. */ -export interface Action { +export type Action = { type: T } From 5c518a66686f106e1b8322fad9bc6a3729bb3668 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 16 May 2023 14:08:58 +0100 Subject: [PATCH 7/8] add comment explaining why Action needs to be a type alias --- src/types/actions.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/actions.ts b/src/types/actions.ts index 16106e2e54..fc85200187 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -14,6 +14,8 @@ * * @template T the type of the action's `type` tag. */ +// things break if this is an interface #justtypescriptthings +// https://tsplay.dev/wj8X6m export type Action = { type: T } From 61515daf7f787798ff4d558bfcb3bbcb7bce43b2 Mon Sep 17 00:00:00 2001 From: "ben.durrant" Date: Tue, 16 May 2023 22:34:44 +0100 Subject: [PATCH 8/8] update comment with TS issue --- src/types/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/actions.ts b/src/types/actions.ts index fc85200187..39313115c0 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -14,8 +14,8 @@ * * @template T the type of the action's `type` tag. */ -// things break if this is an interface #justtypescriptthings -// https://tsplay.dev/wj8X6m +// this needs to be a type, not an interface +// https://github.com/microsoft/TypeScript/issues/15300 export type Action = { type: T }