typescript - 使用具有强类型字段的对象设置通用类型可选映射
问题描述
我正在尝试编写代码来注册强类型数据加载器,但是我在使用打字稿来正确设置地图时遇到了麻烦。在下面的示例中,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
意味着T
可以使用以下四种类型中的任何一种来指定:、、、或never
(即)。如果可以告诉编译器可能是,或者可能是,但不能是它们的联合,那么编译器可能会意识到您的分配适用于其中的每一个并接受它们。E.A
E.B
E.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]
。但是因为您试图为该类型分配一个值,所以编译器要求分配的值是这些类型的交集而不是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>
never
undefined
.i
右边的类型是I<E.A> | I<E.B>
,不能赋值给undefined
,所以有错误。i
例如,如果我们有两个值和,则此错误是有意义的j
,它们都是类型I<E.A> | I<E.B>
并试图分配M[i.type] = j
;。编译器会理所当然地抱怨说,既然j
不可能同时是I<E.A>
andI<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>
而i
is of typeI<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
。这可能是我最喜欢的,因为它最接近于表达你想要做的事情。
另一种可能的解决方法是显式缩小范围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 中)。
推荐阅读
- html - HTML/CSS 如何删除按钮图标
- python - 如何解析这个文本文件?
- python - 如何使用 ElementTree 从深度嵌套的 XML 子元素中提取属性?
- javascript - 偏移量返回上一个位置(在排序之前) - JQuery UI
- r - 基于 r 中单独列中的单个输入删除列中的一组输入
- woocommerce - WooCommerce 产品导入元数据包
- javascript - 无法删除 Typeahead react-bootstrap 中的输入内容
- node.js - Jest 挂在 Supertest 的 POST 上并且不执行“posttest”脚本
- python - 计算两个列表的所有 IoU 的有效方法
- python - 程序一直说'str'对象不可调用