TypeScript中如何让UserStep通过andThen组合后正确推断类型?
解决TypeScript中UserStep组合后的类型推断问题
你的问题本质是索引类型与交叉类型的推导冲突:当UserStep继承Step时,把输出类型固定成了{ [k in K]: User }这种索引类型,而TypeScript默认不会自动把索引类型和其他对象类型的交叉结果展开成我们期望的合并对象,再加上原andThen方法里SubType泛型的约束,进一步让类型推导的结果不符合预期。
下面给你两种针对性的解决方案,按需选择:
方案一:让UserStep保留具体输出类型(推荐,更直观)
直接重构UserStep,让它接受具体的对象类型作为泛型参数,而不是只传键名。这样TypeScript能清晰追踪每一步的输出类型,组合时自然就能正确合并:
type SubType<T, U> = T extends U ? U : never; class Step<A, B> { constructor(private readonly f: (a: A) => B) { } public run(a: A): B { return this.f(a); } public andThen<C, D>(nextStep: Step<SubType<B, C> | B, D>): Step<A, B & D> { return new Step<A, B & D>((state: A) => { const b = this.f(state); return { ...b, ...nextStep.run(b) }; }); } } type User = { id: number, name: string }; // 修改UserStep,约束输出类型必须是带任意键的User对象 class UserStep<T extends Record<string, User>> extends Step<{}, T> { } // 创建实例时,TypeScript会自动推断每个UserStep的具体输出类型 const user21 = new UserStep((input: {}) => ({ user1: { id: 1, name: "A" } })); const user22 = new UserStep((input: {}) => ({ user2: { id: 2, name: "B" } })); // 现在all2的类型会正确推断为 { user1: User; user2: User } const all2 = user21.andThen(user22).run({})
方案二:保留键名定义,优化交叉类型展开
如果你想继续通过键名K来定义UserStep,可以添加一个Expand类型来展开交叉类型,让TypeScript能正确识别合并后的对象结构:
type SubType<T, U> = T extends U ? U : never; // 自定义类型:把交叉类型展开为普通对象类型 type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never; class Step<A, B> { constructor(private readonly f: (a: A) => B) { } public run(a: A): B { return this.f(a); } // 修改andThen的返回类型,用Expand展开交叉结果 public andThen<C, D>(nextStep: Step<SubType<B, C> | B, D>): Step<A, Expand<B & D>> { return new Step<A, Expand<B & D>>((state: A) => { const b = this.f(state); return { ...b, ...nextStep.run(b) }; }); } } type User = { id: number, name: string }; // 保留原有的UserStep定义 class UserStep<K extends string> extends Step<{}, { [k in K]: User }> { } const user21 = new UserStep<"user1">((input: {}) => ({ user1: { id: 1, name: "A" } })); const user22 = new UserStep<"user2">((input: {}) => ({ user2: { id: 2, name: "B" } })); // 现在all2的类型会正确推断为 { user1: User; user2: User } const all2 = user21.andThen(user22).run({})
两种方案都能解决你的问题:方案一更直观,减少了泛型层级;方案二则保留了你原本通过键名抽象UserStep的设计。
内容的提问来源于stack exchange,提问作者ksaveljev
相关产品推荐
相关产品推荐

