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

如何实现支持扩展运算符的TypeScript泛型映射函数,保证返回类型匹配输入类型

问题描述

我想要创建一个可批量更新多种类型实体的函数,目前尝试过两个版本,但一个版本在应该报错时未触发TypeScript错误,另一个在不该报错时却触发了错误。现附上其中一个版本的代码示例,期望示例中的合法代码能通过TypeScript校验,错误赋值能触发预期的类型错误,请问我哪里出错了?

【期望达成目标】以下代码不应产生TypeScript错误:

type EntityA = { id: string; createdAt: Date, a: { nested: string; } };
type EntityB = { id: string; createdAt: Date; b: { nested: number; } };

let entitiesA: EntityA[] = [{id: "1", createdAt: new Date(), a: { nested: "abc" }}];
let entitiesB: EntityB[] = [{id: "2", createdAt: new Date(), b: { nested: 123 }}];

const updateAllEntities = (
    updater: <TEntity extends EntityA | EntityB>(entiy: TEntity)=>TEntity
)=>{
    entitiesA = entitiesA.map(entity => updater(entity));
    entitiesB = entitiesB.map(entity => updater(entity));
};

updateAllEntities((entity) => {
    return { 
        ...entity,
        createdAt: new Date()
    };
});

updateAllEntities((entity) => {
    if ("a" in entity) {
        return { ...entity, a: { nested: "abx" } }
    }
    if ("b" in entity) {
        return { ...entity, b: { nested: 123 } }
    }

    throw new Error("Unreachable");
});

updateAllEntities((entity) => {
    return { 
        ...entity,
        // @ts-expect-error wrong type
        a: 123,
        // @ts-expect-error wrong type
        b: 123
    };
});
问题分析与解决方案

你当前的updateAllEntities用了泛型参数TEntity,但这个泛型是由函数内部调用updater的时机(也就是entitiesA.map和entitiesB.map)来推断的,而非由传入的updater本身约束。这就导致TypeScript没法正确校验updater的输入输出是否严格匹配实体类型,出现两种异常:

  1. 错误赋值时无法识别类型不匹配(比如给EntityB加a:123不会报错);
  2. 合法分支逻辑可能被误判为类型不兼容。

正确实现方式

要解决问题,得把updater定义为能处理所有实体类型的函数,用EntityA | EntityB作为输入输出的联合类型,让TypeScript通过类型守卫自动推断分支里的实体类型:

type EntityA = { id: string; createdAt: Date, a: { nested: string; } };
type EntityB = { id: string; createdAt: Date; b: { nested: number; } };

let entitiesA: EntityA[] = [{id: "1", createdAt: new Date(), a: { nested: "abc" }}];
let entitiesB: EntityB[] = [{id: "2", createdAt: new Date(), b: { nested: 123 }}];

// 修改updater类型:接收联合类型,返回联合类型
const updateAllEntities = (
    updater: (entity: EntityA | EntityB) => EntityA | EntityB
)=>{
    // 添加类型断言,因为TypeScript无法自动推断map后数组类型完全匹配原类型
    entitiesA = entitiesA.map(entity => updater(entity) as EntityA);
    entitiesB = entitiesB.map(entity => updater(entity) as EntityB);
};

// 合法代码:正常通过校验
updateAllEntities((entity) => {
    return { 
        ...entity,
        createdAt: new Date()
    };
});

// 合法代码:通过类型守卫正确推断实体类型,分支返回值匹配
updateAllEntities((entity) => {
    if ("a" in entity) {
        return { ...entity, a: { nested: "abx" } }
    }
    if ("b" in entity) {
        return { ...entity, b: { nested: 123 } }
    }

    throw new Error("Unreachable");
});

// 错误赋值:触发TypeScript错误,符合预期
updateAllEntities((entity) => {
    return { 
        ...entity,
        // TypeScript报错:类型“number”不能赋值给类型“{ nested: string; } | undefined”
        a: 123,
        // TypeScript报错:类型“number”不能赋值给类型“{ nested: number; } | undefined”
        b: 123
    };
});

关键修改说明

  1. 替换泛型为联合类型函数:强制updater必须能处理EntityA和EntityB两种类型,同时通过"a" in entity这类类型守卫缩小分支内的类型范围,保证返回值类型正确。
  2. 添加类型断言:map后加as EntityA/as EntityB,因为TypeScript没法确定updater返回的联合类型具体是哪个子类型,但业务逻辑能保证:传入EntityA时updater返回的一定是EntityA,传入EntityB时返回的一定是EntityB。

原泛型写法的问题

原泛型定义<TEntity extends EntityA | EntityB>(entity: TEntity) => TEntity意味着:调用updater时可以传EntityA | EntityB的任意子类型,且返回该子类型。但在updateAllEntities内部,我们分别传入EntityA和EntityB,TypeScript会推断两次调用的TEntity分别是EntityA和EntityB,但传入的updater需要同时满足这两种泛型约束,这就导致TypeScript无法严格校验updater内部的错误赋值(比如给EntityB加a属性,因为泛型允许TEntity是EntityA,所以不会报错)。

内容的提问来源于stack exchange,提问作者GreenTea222

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 22:00:12