TypeScript中Redux AnyAction不接受额外属性的问题排查
先还原你遇到的代码场景和具体报错:
//********** From Redux type declarations ************ export interface Action<T = any> { type: T } export interface AnyAction extends Action { // Allows any extra properties to be defined in an action. [extraProps: string]: any } //*********************************** type ActionHandler<TState> = (state: TState, action: AnyAction) => TState interface SomeState { prop1: number prop2: string } interface FooAction extends Action { prop2: string } // This also doesnt work // interface FooAction extends AnyAction { // prop2: string // } const foo: ActionHandler<SomeState> = (state: SomeState, action: FooAction): SomeState => { return { ...state, prop2: action.prop2 } }
报错信息:
Type '(state: SomeState, action: FooAction) => SomeState' is not assignable to type 'ActionHandler
'. Types of parameters 'action' and 'action' are incompatible. Property 'prop2' is missing in type 'AnyAction' but required in type 'FooAction'.ts(2322)
核心原因:TypeScript的函数参数逆变 + AnyAction的索引签名特性
你可能会疑惑:AnyAction明明有[extraProps: string]: any的索引签名,允许任意额外属性,为什么还会说它没有prop2?
问题出在两个关键点上:
函数参数的逆变性质:当你把一个函数赋值给另一个函数类型时,TypeScript遵循逆变规则——接受更宽泛类型(比如
AnyAction)的函数,可以被赋值给接受更具体类型(比如FooAction)的变量,但反过来不行。
你的ActionHandler要求函数能处理任意AnyAction类型的参数,但你写的foo函数只能接受带有prop2的FooAction。如果TypeScript允许这种赋值,那么当别人用一个没有prop2的AnyAction调用foo时,函数里访问action.prop2就会出现运行时错误,所以TypeScript会直接阻止这种不安全的赋值。AnyAction的索引签名不保证属性存在:
[extraProps: string]: any只是说“这个类型可以有任意字符串键的属性”,但不保证某个具体的键(比如prop2)一定存在。而FooAction明确要求prop2必须存在,所以AnyAction和FooAction之间并不满足“AnyAction是FooAction父类型”的关系——反过来,FooAction是AnyAction的子类型,但函数参数逆变的规则导致你不能把接受子类型的函数赋值给接受父类型的变量。
解决方法
这里有几种常见的修复方式,你可以根据场景选择:
1. 让ActionHandler支持泛型的Action类型
把ActionHandler改成接受特定的Action类型,而不是固定用AnyAction:
type ActionHandler<TState, TAction extends Action> = (state: TState, action: TAction) => TState const foo: ActionHandler<SomeState, FooAction> = (state, action) => { return { ...state, prop2: action.prop2 } }
这样foo就明确只能处理FooAction类型的action,类型完全匹配,不会有兼容性问题。
2. 在函数内部做类型检查(如果必须用AnyAction)
如果你确实需要让foo兼容AnyAction,那必须在函数里确保action确实包含prop2,避免运行时错误:
const foo: ActionHandler<SomeState> = (state, action) => { // 结合type字段判断会更严谨,比如假设FooAction的type是'FOO_ACTION' if (action.type === 'FOO_ACTION' && 'prop2' in action && typeof action.prop2 === 'string') { return { ...state, prop2: action.prop2 } } // 如果不是预期的action,返回原状态 return state }
3. 使用Redux Toolkit简化类型处理
Redux Toolkit(RTK)已经内置了完善的类型支持,不需要手动定义AnyAction和ActionHandler这类类型,它会自动根据你定义的action生成对应的类型。比如用createSlice:
import { createSlice } from '@reduxjs/toolkit' interface SomeState { prop1: number prop2: string } const initialState: SomeState = { prop1: 0, prop2: '' } const someSlice = createSlice({ name: 'some', initialState, reducers: { updateProp2: (state, action: { payload: string }) => { state.prop2 = action.payload } } }) // 自动生成的action类型和reducer都是类型安全的 export const { updateProp2 } = someSlice.actions export default someSlice.reducer
这种方式完全避开了手动处理AnyAction的类型问题,是Redux官方推荐的写法。
内容的提问来源于stack exchange,提问作者Ilan

