TypeScript泛型类型在联合类型中无法用属性作为数据键的问题
我需要定义多个结构相似的组件类型,结构如下:
{ type: componentName data: { [componentName]: {...} } }
这个结构由Proto文件的oneOf定义生成,无法修改。我基于componentName和Proto生成的类型构建了泛型,单个组件的类型可以正常推断:
// 用生成的类型定义组件结构 // 每个组件都有一个`type`,作为`data`中的键。`$case`是tcproto生成的,用于类型定义 export type ComponentInterface<ComponentName extends Required<ComponentResponse>['data']['$case']> = { type: ComponentName, data: ComponentName extends keyof Extract<Required<ComponentResponse>['data'], { $case: ComponentName }> ? Omit<Extract<Required<ComponentResponse>['data'], { $case: ComponentName }>, '$case'> : never } // 定义两个组件,所有属性都能正确推断 const text: ComponentInterface<'text'> = { type: 'text', data: { text: { attributes: { htmlText: 'Text', }, meta: { layoutPosition: 'main', }, }, }, }; // 单个组件下通过`type`访问数据没问题 console.log(text.data[text.type].attributes?.htmlText);
但当我创建包含这些组件的数组并循环,试图通过componentName访问数据时,TypeScript报错:
// 所有组件的联合类型 type ComponentVariants = ComponentInterface<'headline'> | ComponentInterface<'text'> // 组件数组 const components: ComponentVariants[] = [text, headline] components.map((component) => { console.log(component.data[component.type]); });
错误提示:
Element implicitly has an 'any' type because expression of type "headline" | "text" can't be used to index type 'Omit<{ $case: "text"; text: TextData; }, "$case"> | Omit<{ $case: "headline"; headline: HeadlineData; }, "$case">'.
Property 'headline' does not exist on type 'Omit<{ $case: "text"; text: TextData; }, "$case"> | Omit<{ $case: "headline"; headline: HeadlineData; }, "$case">'.
我认为TypeScript应该能推断出每个组件可以通过type属性访问对应数据,请问问题出在哪?
原因
TypeScript处理联合类型时,无法自动关联component.type和component.data的类型绑定关系。虽然每个联合成员内部type和data是匹配的,但遍历联合数组时,TypeScript会将component.type视为所有可能值的联合("headline" | "text"),同时将component.data视为所有data类型的联合,此时它无法确定当前type对应的键一定存在于当前data中,因此抛出类型错误。
解决方法
方法1:重构类型,生成严格绑定的可辨识联合
直接基于Proto生成的类型构建联合类型,确保每个成员的type和data严格对应:
// 从生成的类型中自动提取所有组件的联合类型 type ComponentVariants = { [K in Required<ComponentResponse>['data']['$case']]: { type: K; data: Omit<Extract<Required<ComponentResponse>['data'], { $case: K }>, '$case'>; }; }[Required<ComponentResponse>['data']['$case']];
使用这个类型定义数组后,遍历过程中TypeScript能正确推断type与data的关联:
const components: ComponentVariants[] = [text, headline]; components.map((component) => { // 类型推断正常,可直接访问 console.log(component.data[component.type]); });
方法2:用类型守卫缩小范围
如果不想重构类型,可编写类型守卫函数明确当前组件的类型:
function isComponent<T extends ComponentVariants['type']>( component: ComponentVariants, type: T ): component is ComponentInterface<T> { return component.type === type; } components.map((component) => { if (isComponent(component, 'text')) { console.log(component.data.text); } else if (isComponent(component, 'headline')) { console.log(component.data.headline); } });
方法3:泛型函数保留类型关联
将遍历逻辑封装为泛型函数,让TypeScript处理每个元素时保留类型绑定:
function processComponent<T extends ComponentVariants['type']>( component: ComponentInterface<T> ) { console.log(component.data[component.type]); } components.forEach(processComponent);
内容的提问来源于stack exchange,提问作者kruben

