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

xvw2m8pv  于 2023-04-07  发布在  TypeScript
关注(0)|答案(1)|浏览(150)

我遇到过这样的问题: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
*/
83qze16e

83qze16e1#

TypeScript不适合处理 * 相关的联合类型 *,如microsoft/TypeScript#30581中所述,您尝试编写一个单一的代码块,该代码块应该同时为某个联合类型的多个成员工作。microsoft/TypeScript#30769中实现的可靠性改进只会使这个缺陷更加明显,错误地选择了安全而不是不安全。
如果你想解决这个问题并让编译器验证你的代码的类型安全性,你需要保留联合并将单个代码块改为每个联合成员一个块;或者重构以保留单个代码块,并将联合更改为约束于联合的generic类型。后一种方法是处理相关联合的推荐方法,在microsoft/TypeScript#47109中有详细描述。
如果你 * 不 * 关心类型安全,你可以使用类型Assert来推进并继续。(这比//@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]行应该总是正确的,但是编译器抱怨一个union不是一个交集。这里的问题是编译器通过比较表达式的 types 而不是键kidentity 来分析f[k] = f[k]。从类型系统的Angular 来看,等号的右手边是从Foo的键keyof Foo处 * 阅读 * 属性,并且由于这是读取位置,所以它变成并集。左手边是将属性 * 写入 * 到Foo的键keyof 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]的唯一安全方法是使用某个值,该值适用于Fooevery 属性(交集)。
区别与键的 identity 有关,编译器不会跟踪它们。它看不到f[k] = f[k]f[k2] = f[k1]之间的区别,并且由于Microsoft/TypeScript#30769在TypeScript 3.5中引入,它会抱怨 both。在TypeScript 3.4和之前的版本中,union()unionOops()都可以正确编译。这两种行为都是不正确的,但是旧的行为有时太宽松,而新的行为有时太严格。
那么,我们如何以一种(大多数情况下)类型安全的方式来处理它呢?如果你想保留联合类型,唯一的方法就是使用narrowing将联合拆分为case,并单独处理每个case。这给了我们多余的东西:

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)可分配给索引访问是不实际的,所以它变成了一个游戏,为了让这个特性有用,允许 * 哪个 * 不合理。(并且,通过扩展,任何T[K]对于相同的TK)根据定义是可赋值给自身的,这是我们允许的基本不可靠性。
虽然这是一个漏洞,但它至少更容易修补,因为如果你有两个 * 可能不相关的 * 泛型键k1k2,你可能也有两个泛型类型参数K1K2

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

当然,如果你只是想保留你的原始代码并继续前进,你可以使用类型Assert来告诉编译器停止打扰你:

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);

Playground链接到代码

相关问题