TypeScript 泛型:泛型属性的联合

TypeScript generics: union of generic properties

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

这是一个演示问题的简化示例(因此它可能缺少一些具有正确类型限制等的额外内容)。我们对 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 应该处理用户的创建(或它需要做的任何其他工作),我希望它 return 具有键 K extends string 的对象中的结果。再一次,所有类型都自动检查并且似乎正常工作但是 all2 存在问题,其中类型被推断 不正确 (很可能它是正确的,但它不同于预期的)。我知道这很可能与 UserStep 的定义方式有关,它的键是 K extends string 但我看不出我可以采取什么其他方法来实现这个 UserStep按预期工作。

所以问题是: 有没有一种方法可以抽象 UserStep 以便它 return 使用不同的键可以得到任何它想要的结果,以便在使用 andThen 类型检查器编写步骤后推断出 correct/expected 类型?

UPDATE:以下错误已在最新版本的 TypeScript 中 fixed 并且很可能与 TS3.8 一起发布。到那时,您应该可以只使用下面 Step<A, B> 的 "most reasonable typing",无需联合或 SubType 解决方法,一切都应该正常工作。


这里的根本问题是类型检查器没有意识到 Step<A, B> 的最合理类型,即:

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

A中是contravariant。这意味着 Step<T, B> 可分配给 Step<U, B> 当且仅当 U 可分配给 T。可分配方向的开关是 "contra" 部分。如果方向相同,如“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 本身,并且在检查此类递归泛型类型中类型参数的方差时,类型检查器假定它是协变的,如 checker.ts:

typeArgumentsRelatedTo() 函数内的注释中所述

// When variance information isn't available we default to covariance

(多亏了jack-williams for his helpful comment

这是错误或设计限制;理想情况下,编译器不会做出任何默认猜测,而是检查结构兼容性。我认为围绕这​​个特定问题没有任何现有的 GitHub 问题,所以我已经提交 microsoft/TypeScript#35805 to ask about it. The general lack of ability for developers to add variance hints is covered in 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;}> !!

最简单的 "fix" 就是手动指定它不能正确推断的类型参数:

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

所以这可能是我的建议。不过,你真的应该测试一下,因为经常会出现疯狂的边缘情况。希望方差问题最终会被修复 "the right way",但至少现在你有一些选择。


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

Playground Link