首页 > 解决方案 > 使用具有强类型字段的对象设置通用类型可选映射

问题描述

我正在尝试编写代码来注册强类型数据加载器,但是我在使用打字稿来正确设置地图时遇到了麻烦。在下面的示例中,M是服务地图,k是服务列表,其字段类型为 的确定值E。但是,当我尝试将实例分配给地图时,它给了我一个错误,说我正在尝试分配给未定义的。不知道从这里去哪里。

enum E {
  A = 'a',
  B = 'b',
}

interface I<T extends E> {
  type: T;
}

type J<T> = T extends E ? I<T> : never;
export type K = J<E>;

const M: { [T in E]?: I<T> } = {};
const k: K[] = [];

k.forEach(
  <T extends E>(i: I<T>) => {
    M[i.type] = i;
    // ERROR
    // Type 'I<T>' is not assignable to type '{ a?: I<E.A> | undefined; b?: I<E.B> | undefined; }[T]'.
    //  Type 'I<T>' is not assignable to type 'undefined'.
  });

标签: typescripttypescript-generics

解决方案


这是当前 TypeScript 编译器的限制,或者可能是多个此类限制的组合。我在这里看到的相关问题是:

  • microsoft/TypeScript#27808(支持extends_oneof泛型约束))和microsoft/TypeScript#25879(支持非泛型映射类型的泛型索引,正是您上面的用例):

    目前没有办法说约束到联合的类型参数(如您T extends E上面的)一次只能采用联合的单个元素。现在,T extends E意味着T可以使用以下四种类型中的任何一种来指定:、、、或never(即)。如果可以告诉编译器可能是,或者可能是,但不能是它们的联合,那么编译器可能会意识到您的分配适用于其中的每一个并接受它们。E.AE.BE.A | E.BETE.AE.B

    但这不是发生的事情。 T可以用 指定E.A | E.B,因此 M[i.type] = i必须适用于这种情况。因此,您的通用回调函数也可以更改为其非通用版本

    k.forEach((i: I<E>) => { M[i.type] = i; }); // error!
    // --------------------> ~~~~~~~~~
    // Type 'I<E>' is not assignable to type 'undefined'
    

    它所做的一切都很好。由于其他原因,此版本也会出错:

  • microsoft/TypeScript#30581(支持相关类型),microsoft/TypeScript#25051(支持分布式控制流分析):

    让我们假设它i的类型是I<E.A> | I<E.B>。不幸的是,编译器无法执行必要的高阶推理来确保这M[i.type] = i是可以接受的。在分配的左侧,M[i.type]可以看到类型为M[E.A] | M[E.B]。但是因为您试图为该类型分配一个值,所以编译器要求分配的值是这些类型的交集而不是union。这是通过microsoft/TypeScript#30769在 TypeScript 3.5 中引入的,作为提高安全性的一种方式。所以在左侧被视为类型,即。因为是,这减少到只是M[i.type]M[E.A] & M[E.B](I<E.A> | undefined) & (I<E.B> | undefined)I<E.A> & I<E.B>neverundefined. i右边的类型是I<E.A> | I<E.B>,不能赋值给undefined,所以有错误。

    i例如,如果我们有两个值和,则此错误是有意义的j,它们都是类型I<E.A> | I<E.B>并试图分配M[i.type] = j;。编译器会理所当然地抱怨说,既然j不可能同时是 I<E.A> and I<E.B>,那么将它分配给 是不安全的M[i.type],因为也许i.type !== j.type. 因为编译器只关注明显安全的类型M[i.type] = i,它目前无法区分它和明显不安全之间的区别M[i.type] = j

    我们想告诉编译器的是 和 的类型M[i.type]i相互关联的:即使它们都是联合类型,也不可能M[i.type]是 typeI<E.A>iis of type I<E.B>。但这目前是不可能的。


那么作为一种解决方法,你能做些什么呢?您可以做的最简单的事情(也可能是正确的事情)是使用类型断言知道你在做什么是安全的,所以告诉编译器不要担心它。最简单的断言是把any手榴弹扔进去:

k.forEach(
  (i) => {
    M[i.type] = i as any;
  });

您可以使用一些不太安全的断言:

k.forEach(
  <T extends E>(i: I<T>) => {
    M[i.type] = i as any as (typeof M)[T];
  });

您保留通用回调并告诉编译器分配是安全的,或者:

k.forEach(
  <T extends E>(i: I<T>) => {
    (M as { [K in T]?: I<K> })[i.type] = i;
  });

您保留通用回调并告诉编译器M可以将其视为只有一个 key 类型的地方T。这可能是我最喜欢的,因为它最接近于表达你想要做的事情。

另一种可能的解决方法是显式缩小范围iI<E.A>然后I<E.B>编译器的控制流分析可以接管:

k.forEach(i => i.type === E.A ?
  M[i.type] = i :
  M[i.type] = i
)

显然,这是多余的,而不是您想要做的。但它确实表明编译器原则上可以理解这M[i.type] = i是安全的,如果可以告诉编译器假装执行了这样的案例枚举(如在 microsoft/TypeScript#25051 中)。

Playground 代码链接


推荐阅读