TypeScript extends类型代码编译失败:IReduxAction继承逻辑存疑求助
Hey Dave, let's break down why you're hitting this TypeScript error even though your action types extend IReduxAction. I'll use a typical example matching your scenario to explain the root cause and fix it.
The Common Scenario (Matching Your Issue)
First, let's replicate the code that triggers the error:
// Base Redux action interface interface IReduxAction { type: string; } // Action type extending IReduxAction interface ActionA extends IReduxAction { type: 'ACTION_A'; payload: number; } // Another action type extending IReduxAction interface ActionB extends IReduxAction { type: 'ACTION_B'; payload: string; } // Union of all action types type AllActions = ActionA | ActionB; // Reducer function function reducer(state: number = 0, action: IReduxAction) { switch (action.type) { case 'ACTION_A': // ❌ Error: Property 'payload' does not exist on type 'IReduxAction' return state + action.payload; case 'ACTION_B': return state; default: return state; } }
Why This Happens
Even though ActionA and ActionB extend IReduxAction, when you annotate the action parameter as IReduxAction, TypeScript only knows about the properties defined in the base interface (type: string). It can't narrow down the type to ActionA inside the case 'ACTION_A' block because:
- The base
IReduxActionhas a broadtype: stringinstead of a literal type union. - TypeScript needs a discriminated union to safely narrow types, and the base interface doesn't provide a constrained discriminant.
In short: Your reducer is accepting the base IReduxAction type instead of the union of your specific action types, so TypeScript can't infer the extra properties like payload on individual actions.
The Fixes
1. Use the Action Union Type in the Reducer
Instead of annotating action as IReduxAction, use your union of specific actions (AllActions). This lets TypeScript narrow the type correctly in each switch case:
function reducer(state: number = 0, action: AllActions) { switch (action.type) { case 'ACTION_A': // ✅ No error: TypeScript knows this is ActionA, so payload is a number return state + action.payload; case 'ACTION_B': // ✅ TypeScript knows this is ActionB return state + parseInt(action.payload); default: return state; } }
2. Constrain the Base Interface's type Property
If you want to keep using IReduxAction as a base, update it to use a union of your action type literals. This turns it into a discriminated union base:
// Update base interface to include all possible action types interface IReduxAction<T extends string = string> { type: T; } // Action types now specify their literal type interface ActionA extends IReduxAction<'ACTION_A'> { payload: number; } interface ActionB extends IReduxAction<'ACTION_B'> { payload: string; } type AllActions = ActionA | ActionB; // Now you can use IReduxAction with the union, or directly use AllActions function reducer(state: number = 0, action: AllActions) { switch (action.type) { case 'ACTION_A': return state + action.payload; case 'ACTION_B': return state; default: return state; } }
3. Type Narrowing with Type Guards
If you need to keep the action parameter as IReduxAction for some reason, you can use a type guard to explicitly narrow the type:
function isActionA(action: IReduxAction): action is ActionA { return action.type === 'ACTION_A'; } function reducer(state: number = 0, action: IReduxAction) { if (isActionA(action)) { // ✅ TypeScript now knows this is ActionA return state + action.payload; } // Handle other cases... return state; }
Key Takeaway
TypeScript's type narrowing relies on having specific, constrained types (like discriminated unions) to infer extra properties. When you use a broad base interface without constraints, TypeScript can't assume that any extending type's properties exist. Using the union of your specific action types is the most straightforward fix for Redux reducers.
内容的提问来源于stack exchange,提问作者Dave Taylor

