diff --git a/packages/toolkit/src/createReducer.ts b/packages/toolkit/src/createReducer.ts index b747cdc6db..06e48467bb 100644 --- a/packages/toolkit/src/createReducer.ts +++ b/packages/toolkit/src/createReducer.ts @@ -15,6 +15,9 @@ import type { NoInfer } from './tsHelpers' */ export type Actions = Record +/** + * @deprecated use `TypeGuard` instead + */ export interface ActionMatcher { (action: AnyAction): action is A } diff --git a/packages/toolkit/src/mapBuilders.ts b/packages/toolkit/src/mapBuilders.ts index 6cda075901..81d8a9aca6 100644 --- a/packages/toolkit/src/mapBuilders.ts +++ b/packages/toolkit/src/mapBuilders.ts @@ -2,9 +2,9 @@ import type { Action, AnyAction } from 'redux' import type { CaseReducer, CaseReducers, - ActionMatcher, ActionMatcherDescriptionCollection, } from './createReducer' +import type { TypeGuard } from './tsHelpers' export interface TypedActionCreator { (...args: any[]): Action @@ -96,9 +96,9 @@ const reducer = createReducer(initialState, (builder) => { }); ``` */ - addMatcher( - matcher: ActionMatcher | ((action: AnyAction) => boolean), - reducer: CaseReducer + addMatcher( + matcher: TypeGuard | ((action: any) => boolean), + reducer: CaseReducer ): Omit, 'addCase'> /** @@ -167,9 +167,9 @@ export function executeReducerBuilderCallback( actionsMap[type] = reducer return builder }, - addMatcher( - matcher: ActionMatcher, - reducer: CaseReducer + addMatcher( + matcher: TypeGuard, + reducer: CaseReducer ) { if (process.env.NODE_ENV !== 'production') { if (defaultCaseReducer) { diff --git a/packages/toolkit/src/tests/mapBuilders.typetest.ts b/packages/toolkit/src/tests/mapBuilders.typetest.ts index b807e0f850..ae930f157a 100644 --- a/packages/toolkit/src/tests/mapBuilders.typetest.ts +++ b/packages/toolkit/src/tests/mapBuilders.typetest.ts @@ -1,9 +1,9 @@ -import type { SerializedError } from '@internal/createAsyncThunk'; +import type { SerializedError } from '@internal/createAsyncThunk' import { createAsyncThunk } from '@internal/createAsyncThunk' import { executeReducerBuilderCallback } from '@internal/mapBuilders' import type { AnyAction } from '@reduxjs/toolkit' import { createAction } from '@reduxjs/toolkit' -import { expectType } from './helpers' +import { expectExactType, expectType } from './helpers' /** Test: alternative builder callback for actionMap */ { @@ -56,10 +56,34 @@ import { expectType } from './helpers' expectType>(action) }) + { + // action type is inferred when type predicate lacks `type` property + type PredicateWithoutTypeProperty = { + payload: number + } + + builder.addMatcher( + (action): action is PredicateWithoutTypeProperty => true, + (state, action) => { + expectType(action) + expectType(action) + } + ) + } + // action type defaults to AnyAction if no type predicate matcher is passed builder.addMatcher( () => true, (state, action) => { + expectExactType({} as AnyAction)(action) + } + ) + + // with a boolean checker, action can also be typed by type argument + builder.addMatcher<{ foo: boolean }>( + () => true, + (state, action) => { + expectType<{ foo: boolean }>(action) expectType(action) } ) diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index e7d622e8c3..e91457326e 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -101,8 +101,12 @@ export type NoInfer = [T][T extends any ? 0 : never] export type Omit = Pick> +export interface TypeGuard { + (value: any): value is T +} + export interface HasMatchFunction { - match: (v: any) => v is T + match: TypeGuard } export const hasMatchFunction = ( @@ -112,7 +116,7 @@ export const hasMatchFunction = ( } /** @public */ -export type Matcher = HasMatchFunction | ((v: any) => v is T) +export type Matcher = HasMatchFunction | TypeGuard /** @public */ export type ActionFromMatcher> = M extends Matcher<