如何在React Reducer中根据action.type映射对应state键的value类型?
给定初始状态:
const initialState = { hasError: false, counter: 0, token: [...Array(2)].map(() => ''), };
以及类似React官方示例的reducer结构(修改为使用action.value而非依赖当前状态值):
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: action.value + 1}; case 'decrement': return {count: action.value - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement', value: 1})}>-</button> <button onClick={() => dispatch({type: 'increment', value: -1})}>+</button> </> ); }
需要实现的是:让reducer根据action.type自动推导action.value的类型。比如处理'hasError'类型的action时,action.value应自动推导为boolean;处理'token'类型时,action.value自动推导为string[]。
用户尝试过以下两种写法,但均未达到预期效果:
第一种写法:
const reducer = <K extends keyof typeof initialState>(state: typeof initialState, action: { type: K; value: typeof initialState[K] }): typeof initialState => { switch (action.type) { case 'hasError': { return { ...state, hasError: action.value, // 期望此处类型为`boolean`,但未正确推导 }; } case 'token': { return { ...state, token: action.value, // 期望此处类型为`string[]`,但未正确推导 } // 其他情况... } };
第二种写法:
type TypeResolver<K> = Extract<TypeMap[keyof TypeMap], { state: K }>['valueType']; type TypeObject<K extends keyof typeof initialState> = { state: K, valueType: typeof initialState[K] }; type TypeMap = { error: TypeObject<'hasError'>; resend: TypeObject<'resendEnabled'>; timerIteration: TypeObject<'timerIteration'>; }; const reducer = <K extends keyof TypeMap>(state: typeof initialState, action: { type: K; value: TypeResolver<K> }): typeof initialState => { //... }
核心思路是创建包含所有合法action的联合类型,让TypeScript通过switch语句的类型收窄自动推导对应value的类型,替代泛型方案。
步骤1:定义Action联合类型
基于initialState的键,自动生成每个键对应的action类型,再组合成联合类型:
type State = typeof initialState; type Action = { [K in keyof State]: { type: K; value: State[K] } }[keyof State];
这段代码会自动生成如下联合类型:
type Action = | { type: 'hasError'; value: boolean } | { type: 'counter'; value: number } | { type: 'token'; value: string[] };
步骤2:编写带类型推导的reducer
将reducer的action参数指定为Action类型,此时在switch的每个case分支里,TypeScript会自动收窄action的类型,从而准确推导action.value的具体类型:
const reducer = (state: State, action: Action): State => { switch (action.type) { case 'hasError': { return { ...state, hasError: action.value, // 自动推导为boolean类型 }; } case 'counter': { return { ...state, counter: action.value, // 自动推导为number类型 }; } case 'token': { return { ...state, token: action.value, // 自动推导为string[]类型 }; } default: // 兜底检查,确保覆盖所有action类型 const _exhaustiveCheck: never = action; throw new Error(`未知action类型: ${_exhaustiveCheck.type}`); } };
原泛型写法失效原因
之前的泛型写法K extends keyof State会让TypeScript将K识别为所有键的联合类型,而非单个具体键,因此在switch分支里无法进行类型收窄,导致action.value始终是State[keyof State](即boolean | number | string[]),无法自动推导为对应分支的具体类型。而使用联合类型的Action,每个case分支会明确匹配联合类型中的某一个子类型,TypeScript就能准确完成类型收窄。
内容的提问来源于stack exchange,提问作者Rick Stanley

