TypeScript泛型:泛型属性的并集

问题描述 投票:1回答:1

我有一个步骤的概念。它接受输入并产生输出。

这是一个简化的示例,用于演示该问题(因此,可能缺少一些带有正确类型限制的多余牛肉,等等)。我们对andThen函数感兴趣,该函数在逐步执行时会累积状态。对此代码块没有问题,仅供参考。

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) };
    });
  }
}

下面的方法很好,我们有两个步骤,自动推断类型,最后得到all具有期望的类型{ user1: User, user2: User }

type User = { id: number, name: string };

const user1 = new Step((input: {}) => ({ user1: { id: 1, name: "A" } }));
const user2 = new Step((input: {}) => ({ user2: { id: 2, name: "B" } }));

const all = user1.andThen(user2).run({})

我的下一步是在处理产生结果的相同函数时重用相同的抽象,但是我唯一希望的区别是结果所依据的键。您可能不理解我刚才所说的内容,所以让我向您展示代码:

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" } }));

// inferred type here is wrong { user1: User } instead of { user1: User, user2: User }
const all2 = user21.andThen(user22).run({})

UserStep应该处理用户的创建(或其他需要做的工作),我希望它在键为K extends string的对象中返回结果。再次,所有类型都会自动检查并且似乎可以正常工作,但是all2出现了一个问题,在该问题中,类型[被推断(很可能是正确的,但与预期的有所不同)。我了解到,很可能必须对UserStep的定义方式及其键为K extends string进行某些操作,但是我看不到我可以采取什么其他方法来使UserStep正常工作。

所以问题是:是否有一种抽象UserStep的方法,以便它使用不同的键返回任何想要的结果,以便在使用andThen编写步骤之后,类型检查器可以推断正确/期望的类型?
typescript generics type-inference typechecking
1个回答
1
投票
这里的根本问题是类型检查器没有意识到Step<A, B>的最合理的键入,即:

interface Step<A, B> { f: (a: A) => B; andThen<C>(next: Step<B, C>): Step<A, B & C>; }

contravariant中的A。意味着仅当Step<T, B>可分配给Step<U, B>时,U才可分配给T。可分配性方向上的开关是“相反”部分。如果方向相同,则与“仅当F<T>可分配给F<U>时,T可分配给U”中的方向相同,否则为

协变量。

无论如何,这意味着您在这里遇到错误:

declare const u1: Step<{}, { user1: {} }>; declare const u2: Step<{}, { user2: {} }>; let u1u2 = u1.andThen(u2); // error!? // Type 'Step<{}, any>' is not assignable to type 'Step<{ user1: {}; }, any>'. // Type '{}' is not assignable to type '{ user1: {}; }'.

之所以没有意识到Step<A, B>A中是协变的,是因为Step<A, B>andThen()方法涉及Step本身,并且当检查此类递归泛型类型的类型参数的方差时,类型检查器假定它是协变的,如typeArgumentsRelatedTo()checker.ts函数内部的注释中所述:

checker.ts

((感谢// When variance information isn't available we default to covariance关于此的jack-williams

这是错误或设计限制;理想情况下,编译器不会进行任何默认猜测,而是检查结构兼容性。我认为围绕该特定问题不存在任何GitHub问题,因此我已提交helpful comment进行询问。 microsoft/TypeScript#35805解决了开发人员通常无法添加差异提示的问题。


无论如何,看来您使用了一种变通方法来使microsoft/TypeScript#1394方法不跳动方差检查器,同时仍然使andThen()保持反相关:

A

不幸的是,如您所见,面对诸如    type SubType<T, U> = T extends U ? U : never;
    interface Step<A, B> {
        f: (a: A) => B;
        andThen<C, X>(next: Step<B | SubType<B, X>, C>): Step<A, B & C>;
    }
    declare const u1: Step<{}, { user1: {} }>;
    declare const u2: Step<{}, { user2: {} }>;
    let u1u2 = u1.andThen(u2); // okay
之类的通用条件类型,类型推断可能很混乱:

SubType

最简单的“解决方案”只是手动指定无法正确推断的类型参数:

type User = { id: number, name: string }; interface UserStep<K extends string> extends Step<{}, { [k in K]: User }> { } declare const v1: UserStep<"user1">; declare const v2: UserStep<"user2">; let v1v2 = v1.andThen(v2); // Step<{}, {user1: User;}> !!


思考在相反方面具有相同行为的另一种解决方法是:

let v1v2Fixed = v1.andThen<{ user2: User }, {}>(v2); // okay: // Step<{}, { user1: User;} & { user2: User;}>
但是这里没有通用的条件类型值得担心,并且编译器在推断事物方面要好得多:

interface Step<A, B> { f: (a: A) => B; andThen<C, X>(next: Step<B | X, C>): Step<A, B & C>; }

所以这可能是我的建议。不过,您应该真正进行测试,因为通常会有一些疯狂的案例。希望最终方差问题将被“正确的方式”修复,但至少现在您有一些选择。


好的,希望能有所帮助;祝你好运!

let v1v2 = v1.andThen(v2); // okay: // Step<{}, { user1: User;} & { user2: User;}>

© www.soinside.com 2019 - 2024. All rights reserved.