[英]Setting a generic typed optional map with an object with a strongly typed field
我正在尝试编写代码来注册强类型数据加载器,但是我在使用打字稿无法正确设置地图时遇到了麻烦。 在下面的示例中, 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# extends_oneof
(支持extends_oneof
通用约束))和microsoft/TypeScript#25879 (支持非通用映射类型的通用索引,正是您上面的用例):
目前还没有办法说,一个类型参数约束为工会(像你T extends E
以上)只能走联合的同时一个单独的元素。 现在, T extends E
意味着可以使用以下四种类型中的任何一种来指定T
: never
、 EA
、 EB
或EA | EB
EA | EB
(这只是E
)。 如果有可能,告诉编译器T
可能是EA
,也可能是EB
,但它不能成为他们的工会,那么也许编译器可以知道你的分配工作,对于那些每一个接受两个他们。
但事实并非如此。 T
可以用EA | EB
指定 EA | EB
,所以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<EA> | I<EB>
I<EA> | I<EB>
。 不幸的是,编译器无法执行必要的高阶推理,以确保M[i.type] = i
是可接受的。 在赋值的左侧, M[i.type]
被视为类型M[EA] | M[EB]
M[EA] | M[EB]
。 但是因为您试图为该类型分配一个值,所以编译器要求分配的值是这些类型的交集而不是union 。 这是通过microsoft/TypeScript#30769 在 TypeScript 3.5中引入的,作为提高安全性的一种方式。 因此,左侧的M[i.type]
被视为类型M[EA] & M[EB]
,即(I<EA> | undefined) & (I<EB> | undefined)
。 由于I<EA> & I<EB>
never
,这简化为undefined
。 右边的i
的类型是I<EA> | I<EB>
I<EA> | I<EB>
,它不能分配给undefined
,所以有一个错误。
例如,如果我们有两个值i
和j
,它们都是I<EA> | I<EB>
类型,那么这个错误就会有意义I<EA> | I<EB>
I<EA> | I<EB>
并尝试分配M[i.type] = j
;。 编译器会合理地抱怨,因为j
不可能同时是I<EA>
和I<EB>
,那么将它分配给M[i.type]
是i.type !== j.type
,因为可能i.type !== j.type
。 因为编译器只注重类型的明显安全M[i.type] = i
,它目前还不能告诉大家,和明显的不安全之间的差异M[i.type] = j
。
我们想告诉编译器是的类型M[i.type]
和i
都彼此相关的:尽管他们都是联盟类型,这是不可能的M[i.type]
为类型I<EA>
而i
是I<EB>
类型。 但这目前是不可能的。
那么你能做些什么来解决呢? 您可以做的最简单的事情(可能也是正确的做法)是使用类型断言。 你知道你在做什么是安全的,所以告诉编译器不要担心。 最简单的断言是在那里扔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<EA>
和I<EB>
,此时编译器的控制流分析可以接管:
k.forEach(i => i.type === E.A ?
M[i.type] = i :
M[i.type] = i
)
显然,这是多余的,而不是您想要做的。 但它确实表明编译器原则上可以理解M[i.type] = i
是安全的,如果可以告诉编译器假装执行了这样的案例枚举(如在 microsoft/TypeScript#25051 中) .
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.