React useReducer中Action类型定义引发TS2339错误排查
在React的useReducer中使用formReducer,同时定义了基于联合类型的Action类型。当前遇到TS2339错误:在updated_input和updated_nested_input分支解构data时,提示'value'等属性不存在。尽管UpdatedInputActionData通过交叉类型(或interface扩展)继承了包含这些属性的StatePropObj,但仍报错,使用的TypeScript版本为^3.8.3。
reducer代码
export const formReducer = (state: FormState, action: Action) => { const { data, type } = action switch (type) { case 'updated_input': { const { value, hasError, error, name, isFormValid } = data return { ...state, [name]: { ...(state[name as keyof FormState] as {}), value, hasError, error, }, isFormValid, } } case 'updated_nested_input': { const { value, hasError, error, isFormValid, nestedKey, name } = data return { ...state, [nestedKey]: { ...state[nestedKey], [name]: { ...(state[nestedKey][name as keyof (Address | AdminUser)] as {}), value, hasError, error, nestedKey, }, }, isFormValid, } } case 'focus_out_input': { const { name, isFormValid, touched, hasError, error } = data return { ...state, [name]: { ...(state[name as keyof FormState] as {}), touched, hasError, error, }, isFormValid, } } case 'focus_out_nested_input': { const { touched, isFormValid, nestedKey, name, hasError, error } = data return { ...state, [nestedKey]: { ...state[nestedKey], [name]: { ...(state[nestedKey][name as keyof (Address | AdminUser)] as {}), touched, nestedKey, hasError, error, }, }, isFormValid, } } case 'submit_error': { const { submitErrors } = data return { ...state, submitErrors, } } default: throw new Error(`Unknown action type: ${type}`) } }
Action类型定义
import { AxiosError } from 'axios' export type NestedKey = 'address' | 'adminUser' export interface StatePropObj { value: string error: string nestedKey: NestedKey | boolean touched: boolean hasError: boolean } export type UpdatedInputActionData = StatePropObj & { submitErrors?: AxiosError | boolean isFormValid: boolean name: string } export interface UpdatedNestedInputActionData extends UpdatedInputActionData { nestedKey: NestedKey } export type Action = | { type: 'updated_nested_input'; data: UpdatedInputActionData } | { type: 'submit_error'; data: { submitErrors: AxiosError | boolean } } | { type: 'updated_input'; data: UpdatedInputActionData } | { type: 'focus_out_input' data: { touched: boolean name: string isFormValid: boolean error: string hasError: boolean } } | { type: 'focus_out_nested_input' data: { touched: boolean name: string isFormValid: boolean nestedKey: NestedKey error: string hasError: boolean } }
错误信息示例
属性'value'不存在于类型'UpdatedInputActionData | { submitErrors: boolean | AxiosError; } | { touched: boolean; name: string; isFormValid: boolean; error: string; hasError: boolean; } | { ...; }'上。
49 const { value, hasError, error, name, isFormValid } = data
错误原因
TypeScript 3.8版本的联合类型窄化局限性
TypeScript 3.8对基于type字段的联合类型窄化支持不完善,尽管在switch分支中通过case指定了具体类型,但TS无法精准将action.data从Action联合类型中窄化为对应的UpdatedInputActionData,导致解构时认为data可能是联合类型中的其他成员,进而报错属性不存在。Action类型定义的精准度问题
在Action联合类型中,updated_nested_input对应的data被错误定义为UpdatedInputActionData,而非更精准的UpdatedNestedInputActionData,这会加剧TS的类型推断混乱。
解决方案
方案1:升级TypeScript版本
TypeScript 4.0及以上版本对联合类型的控制流分析(基于switch case的类型窄化)做了大幅优化,升级到4.x版本后,TS能正确识别case分支中action.data的具体类型,错误会自动消失。
方案2:手动类型断言(适配TS 3.8)
若无法升级TS版本,可在分支中对data进行手动类型断言,明确告知TS当前分支的data类型:
case 'updated_input': { const { value, hasError, error, name, isFormValid } = data as UpdatedInputActionData // 后续代码不变 } case 'updated_nested_input': { const { value, hasError, error, isFormValid, nestedKey, name } = data as UpdatedNestedInputActionData // 后续代码不变 }
方案3:修正Action类型定义(配合方案1或2)
将Action类型中updated_nested_input的data类型修正为UpdatedNestedInputActionData,让类型定义更精准:
export type Action = | { type: 'updated_nested_input'; data: UpdatedNestedInputActionData } | { type: 'submit_error'; data: { submitErrors: AxiosError | boolean } } | { type: 'updated_input'; data: UpdatedInputActionData } // 其他类型不变
内容的提问来源于stack exchange,提问作者HelloWorld

