使用具有强类型字段的对象设置泛型类型可选映射



我正在尝试编写代码来注册强类型数据加载器,但是我在打字稿上遇到问题,无法正确设置映射。在下面的示例中,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'.
});

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

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

    目前没有办法说约束到联合的类型参数(如上面的T extends E)一次只能采用联合的一个元素。现在,T extends E意味着可以使用以下四种类型中的任何一种指定TneverE.AE.BE.A | E.B(这只是E)。 如果可以告诉编译器T可能是E.A或者可能是E.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]类型。 但是,由于您尝试为该类型值,因此编译器要求分配的值是这些类型的交集,而不是联合。 这是通过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>never,这减少到只有undefined。 右边的i类型是I<E.A> | I<E.B>,不能分配给undefined,所以有一个错误。

    例如,如果我们有两个值,ij,两者都是I<E.A> | I<E.B>类型并尝试分配M[i.type] = j;,则此错误是有意义的。 编译器会理所当然地抱怨说,既然j不可能I<E.A>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]也不可能是I<E.A>型,而iI<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可以被视为只有T类型的键。 这可能是我最喜欢的,因为它最接近表达你想做的事情。

另一种可能的解决方法是显式地将i缩小到依次I<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 中)。

操场链接到代码

最新更新