我遇到过这样的问题:https://github.com/microsoft/TypeScript/issues/31694
那么解决方案是什么呢?在这个例子中,我如何在没有@ts-ignore
的情况下将任何内容分配给store[clazz]
?
type Entity_A = { id?: number, prop_a: any }
type Entity_B = { id?: number, prop_b: any }
type EntityStorage = {
a_entity: Entity_A[]
b_entity: Entity_B[]
}
type EntityClass = keyof EntityStorage // 'a_entity' | 'b_entity'
type AnyEntityArray = EntityStorage[EntityClass] // Entity_A[] | Entity_B[]
type AnyEntity = NonNullable<EntityStorage[EntityClass]>[number] // Entity_A | Entity_B
const a_exampleEntity: Entity_A = { prop_a: null };
const b_exampleEntity: Entity_B = { prop_b: null };
const store: EntityStorage = {
a_entity: [a_exampleEntity, a_exampleEntity],
b_entity: [b_exampleEntity, b_exampleEntity],
};
// `as EntityClass` cast is required. In actual code it's value from another EntityStorage instance.
const clazz: EntityClass = 'b_entity' as EntityClass; // 'a_entity' | 'b_entity'
let entities: AnyEntityArray; // Entity_A[] | Entity_B[]
// on reading store[clazz] is union
// ↓
// Entity_A[] | Entity_B[] = Entity_A[] | Entity_B[]
entities = store[clazz];
// on writing store[clazz] is union
// ↓
// Entity_A[] & Entity_B[] = Entity_A[] | Entity_B[]
store[clazz] = entities;
/* error says
Entity_A[] | Entity_B[] // union
is not assignable to
Entity_A[] & Entity_B[] // intersection
*/
1条答案
按热度按时间83qze16e1#
TypeScript不适合处理 * 相关的联合类型 *,如microsoft/TypeScript#30581中所述,您尝试编写一个单一的代码块,该代码块应该同时为某个联合类型的多个成员工作。microsoft/TypeScript#30769中实现的可靠性改进只会使这个缺陷更加明显,错误地选择了安全而不是不安全。
如果你想解决这个问题并让编译器验证你的代码的类型安全性,你需要保留联合并将单个代码块改为每个联合成员一个块;或者重构以保留单个代码块,并将联合更改为约束于联合的generic类型。后一种方法是处理相关联合的推荐方法,在microsoft/TypeScript#47109中有详细描述。
如果你 * 不 * 关心类型安全,你可以使用类型Assert来推进并继续。(这比
//@ts-ignore
指令更好,根据文档“只会抑制错误报告,我们建议你非常谨慎地使用[这样的]注解 *”。让我们看看这个简化的问题示例:
看起来
f[k] = f[k]
行应该总是正确的,但是编译器抱怨一个union不是一个交集。这里的问题是编译器通过比较表达式的 types 而不是键k
的 identity 来分析f[k] = f[k]
。从类型系统的Angular 来看,等号的右手边是从Foo
的键keyof Foo
处 * 阅读 * 属性,并且由于这是读取位置,所以它变成并集。左手边是将属性 * 写入 * 到Foo
的键keyof Foo
处,因为这是一个写位置,所以它变成了一个交集。如果你只知道键的 * 类型 *,这是完全正确的做法。例如,你想让它失败:这里,
k1
和k2
是同一类型的不同变量,因此您可能会阅读和写入不兼容的属性。写入f[k2]
的唯一安全方法是使用某个值,该值适用于Foo
的 every 属性(交集)。区别与键的 identity 有关,编译器不会跟踪它们。它看不到
f[k] = f[k]
和f[k2] = f[k1]
之间的区别,并且由于Microsoft/TypeScript#30769在TypeScript 3.5中引入,它会抱怨 both。在TypeScript 3.4和之前的版本中,union()
和unionOops()
都可以正确编译。这两种行为都是不正确的,但是旧的行为有时太宽松,而新的行为有时太严格。那么,我们如何以一种(大多数情况下)类型安全的方式来处理它呢?如果你想保留联合类型,唯一的方法就是使用narrowing将联合拆分为case,并单独处理每个case。这给了我们多余的东西:
没有人愿意这样做;如果你的不同情况实际上需要不同的代码,这会更有意义。但它确实有效。
所以,如果你想让一个代码块只执行一次并进行类型检查,你需要给予使用特定的联合类型,而改为使用限制在联合中的 * 泛型 * 类型。像这样:
这里我们有一个从右边的泛型类型
Foo[K]
到左边的泛型类型Foo[K]
的赋值语句,编译器很高兴并允许它,这时你可能会想:前面关于k1
和k2
的争论是否暗示了这是不安全的?答案是.. * 是的,这是不安全的 *:但是泛型有意支持这种不安全性,如microsoft/TypeScript#30769中的注解所述:
什么都不说(除了
any
)可分配给索引访问是不实际的,所以它变成了一个游戏,为了让这个特性有用,允许 * 哪个 * 不合理。(并且,通过扩展,任何T[K]
对于相同的T
和K
)根据定义是可赋值给自身的,这是我们允许的基本不可靠性。虽然这是一个漏洞,但它至少更容易修补,因为如果你有两个 * 可能不相关的 * 泛型键
k1
和k2
,你可能也有两个泛型类型参数K1
和K2
:当然,如果你只是想保留你的原始代码并继续前进,你可以使用类型Assert来告诉编译器停止打扰你:
它根本不是类型安全的(或者至少编译器不会为您验证它),但它非常方便。
如果你想让编译器对一个代码块进行类型检查,那么这个代码块需要被表示为泛型而不是联合。
在示例代码中,由于您不在函数内部,因此不能只是“使其泛型化”。要做到这一点,您需要创建一个泛型函数来保存代码块,即使只是像下面的IIFE那样立即调用函数:
Playground链接到代码