如何实现支持扩展运算符的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的输入输出是否严格匹配实体类型,出现两种异常:
- 错误赋值时无法识别类型不匹配(比如给
EntityB加a:123不会报错); - 合法分支逻辑可能被误判为类型不兼容。
正确实现方式
要解决问题,得把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 }; });
关键修改说明
- 替换泛型为联合类型函数:强制
updater必须能处理EntityA和EntityB两种类型,同时通过"a" in entity这类类型守卫缩小分支内的类型范围,保证返回值类型正确。 - 添加类型断言:
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

