属性访问器在赋值时创建交集类型而不是并集



我遇到过这个问题: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
*/

TypeScript无法处理相关的并集类型,如microsoft/TypeScript#30581中所述,在该文件中,您试图编写一个代码块,该代码块应该同时适用于某个并集类型的多个成员。在microsoft/TypeScript#30769中实现的健全性改进只会使这一缺陷更加明显,因为它错误地认为是安全的,而不是不安全的。

如果你想解决这个问题,并让编译器验证代码的类型安全性,你要么需要保留联合,要么将单个代码块更改为每个联合成员一个块;或者重构以保留单个代码块,并将并集更改为约束于并集的泛型类型。后一种方法是处理相关并集的推荐方法,在microsoft/TypeScript#47109中有详细描述。

如果不关心类型安全,您可以始终使用类型断言来推进并继续前进。(这比//@ts-ignore指令更可取,根据文档"只抑制错误报告,我们建议您非常谨慎地使用[此类]注释"。)


让我们来看看这个问题的简化示例:

interface Foo {
x: { a: 1 };
y: { b: 2 };
z: { c: 3 };
}
function union(f: Foo, k: keyof Foo) {
f[k] = f[k]; // error!
//~~ <--
// Type '{ a: 1; } | { b: 2; } | { c: 3; }' is not assignable 
// to type '{ a: 1; } & { b: 2; } & { c: 3; }'.
}

f[k] = f[k]行似乎总是正确的,但编译器抱怨并集不是交集。这里的问题是,编译器通过比较表达式的类型而不是密钥kidentity来分析f[k] = f[k]。从类型系统的角度来看,等号的右侧是在键keyof Foo处从Foo读取属性,由于这是读取位置,因此它变成了并集。左手边是在键keyof Foo处向Foo写入属性,因为这是一个写入位置,所以它变成了一个交集。如果你只知道键的类型,那么这是完全正确的做法。例如,您希望此操作失败:

function unionOops(f: Foo, k1: keyof Foo, k2: keyof Foo) {
f[k2] = f[k1]; // error!
//~~~ <--
// Type '{ a: 1; } | { b: 2; } | { c: 3; }' is not assignable 
// to type '{ a: 1; } & { b: 2; } & { c: 3; }'.
}

这里,k1k2是相同类型的不同变量,因此您可能正在读取和写入不兼容的属性。写入f[k2]的唯一安全方法是使用某个值,该值适用于Foo(交集)的每个属性。

差异与键的标识有关,编译器不会跟踪它们。它看不出f[k] = f[k]f[k2] = f[k1]之间的区别,并且由于在TypeScript 3.5中引入了microsoft/TypeScript#30769,它抱怨两者。在TypeScript 3.4及之前的版本中,union()unionOops()都会编译而不会出错。这两种行为都是不正确的,但旧的行为有时过于松懈,而新的行为有时又过于严格。


那么我们如何以(主要)类型安全的方式处理它呢?如果要保留并集类型,唯一的方法是使用窄化将并集拆分为多个事例,并分别处理每个事例。这给了我们多余的东西:

function redundant(f: Foo, k: keyof Foo) {
switch (k) {
case "x": f[k] = f[k]; break; // okay
case "y": f[k] = f[k]; break; // okay
case "z": f[k] = f[k]; break; // okay
}
}

没有人愿意这样做;如果您的不同案例实际需要不同的代码,则更有意义。但它是有效的。


因此,如果您希望一个代码块执行一次并进行类型检查,则需要放弃使用特定的并集类型,转而使用受并集约束的泛型类型。像这样:

function generic<K extends keyof Foo>(f: Foo, k: K) {
f[k] = f[k]; // okay
}

这里我们有一个从右边的泛型类型Foo[K]到左边的泛型类型Foo[K]的赋值。编译器很高兴并允许它。在这一点上,你可能会想:以前关于k1k2的争论难道不意味着这不安全吗?答案是…是的,这不安全:

function genericOops<K extends keyof Foo>(f: Foo, k1: K, k2: K) {
f[k2] = f[k1]; // okay?!
}

但这种不安全性是故意支持泛型的,正如在microsoft/TypeScript#30769:中的这条评论中所描述的那样

说任何东西(除了any)都不可分配给索引访问是不现实的,所以它变成了一个的游戏,为了使该功能有用,不安全。我们一直遵循的一条规则是,任何给定的类型(以及相同TK的任何T[K])根据定义都是可分配给自己的,这是我们允许的基本不确定性。

虽然这是一个漏洞,但它至少更容易修补,因为如果您有两个可能不相关的泛型键k1k2,那么您可能还有两个泛型类型参数K1K2:

function genericOopsFixed<K1 extends keyof Foo, K2 extends keyof Foo>(
f: Foo, k1: K1, k2: K2
) {
f[k2] = f[k1]; // error again
}

当然,如果你只想保留原始代码并继续前进,你可以使用类型断言来告诉编译器不要再打扰你:

function assertion(f: Foo, k: keyof Foo) {
f[k] = f[k] as any; // okay
}

它根本不是类型安全的(或者至少编译器没有为您验证它),但它非常方便。


这就是发生的事情。如果你想让编译器对单个代码块进行多个用例的类型检查,那么多个用例需要表示为泛型,而不是并集。

在您的示例代码中,由于您不在函数内部,因此您不能仅仅"使其通用";。要做到这一点,您需要创建一个通用函数来保存代码块,即使只是像下面的IIFE:那样立即调用该函数

(<K extends EntityClass>(k: K) => {
const entities: EntityStorage[K] = store[k];
store[k] = entities; // okay
})(clazz);

游乐场链接到代码

相关内容

  • 没有找到相关文章

最新更新