TypeScript泛型函数中如何保持关联类型间的连接?
核心问题:泛型函数中丢失type与value的类型关联
你的代码中,runAction函数使用<FAction extends FormAction>作为泛型约束时,TypeScript无法追踪formAction.type对应的setter参数类型和formAction.value的关联——因为FormAction是通过分布式条件类型生成的联合类型,泛型参数FAction可以是该联合类型的任意子集,编译器无法确定二者的对应关系。
解决方案:绑定泛型到具体的ActionType
修改runAction的泛型参数,直接绑定到ActionType,让formAction的类型明确对应单个ActionType的FormAction:
function runAction<ActionT extends ActionType>( formAction: FormAction<ActionT>, form: CForm, ) { const setter = getSetter(formAction.type); setter(formAction.value, form); // 类型检查通过 }
这样TypeScript能明确追踪到ActionT对应的setter参数类型和formAction.value的匹配关系,因为FormAction<ActionT>是针对单个ActionT的具体类型,而非联合类型的模糊子集。
附带问题:移除getSetter中的类型断言
原getSetter依赖类型断言是因为Array.find无法在泛型场景下准确推断返回值类型。可以通过将actions数组转换为键值对映射对象,让TypeScript直接推断出类型关联,无需断言。
实现方式:创建action映射表
// 将actions数组转换为type -> action的映射对象 const actionMap = Object.fromEntries( actions.map(action => [action.type, action]) ) as { [K in ActionType]: Extract<SingleAction, { type: K }> }; function getSetter<ActionT extends ActionType>( actionType: ActionT, ): ActionSetter<ActionT> { const actionItem = actionMap[actionType]; if (!actionItem) { throw new Error("unknown action"); } return actionItem.set; // 无需类型断言,类型自动匹配 }
原理说明
通过Object.fromEntries将数组转换为对象后,我们给actionMap添加了精确的类型定义:每个键是ActionType的成员,对应的值是该type对应的具体SingleAction。此时访问actionMap[actionType]时,TypeScript能直接推断出对应的action类型,自然就能正确返回ActionSetter<ActionT>类型的set函数,无需额外断言。
完整代码示例
type CForm = { config: Config; }; type Config = { projectName: string | undefined; }; // one action per field const actions = [ { type: "init", set: (value: CForm, _form: CForm) => value, }, { type: "setProjectName", set: (value: string, form: CForm): CForm => ({ ...form, config: {...form.config, projectName: value} }), }, ] as const; /** one of the entries in actions */ type SingleAction = (typeof actions)[number]; /** all available action types */ type ActionType = SingleAction["type"]; /** a specific action object from actions array */ type ActionItem<ActionT extends ActionType> = Extract< SingleAction, { type: ActionT } >; /** the setter function corresponding to the action type */ type ActionSetter<ActionT extends ActionType> = ActionItem<ActionT>["set"]; type FormAction<ActionT extends ActionType = ActionType> = ActionT extends unknown ? { type: ActionT; value: Parameters<ActionSetter<ActionT>>[0]; } : never; // 创建action映射表,替代find方法 const actionMap = Object.fromEntries( actions.map(action => [action.type, action]) ) as { [K in ActionType]: Extract<SingleAction, { type: K }> }; function getSetter<ActionT extends ActionType>( actionType: ActionT, ): ActionSetter<ActionT> { const actionItem = actionMap[actionType]; if (!actionItem) { throw new Error("unknown action"); } return actionItem.set; } // 固定类型场景正常工作 const form: CForm = { config: { projectName: undefined } }; const formAction: FormAction<"setProjectName"> = { type: "setProjectName", value: "name", }; const setter = getSetter(formAction.type); setter(formAction.value, form); // 泛型函数类型检查通过 function runAction<ActionT extends ActionType>( formAction: FormAction<ActionT>, form: CForm, ) { const setter = getSetter(formAction.type); setter(formAction.value, form); }
内容的提问来源于stack exchange,提问作者ontanj

