You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 16:35:01