typescript 为什么Map类型中具有可区分联合的交集给予所有类型的联合而不是缩小它?

3htmauhk  于 2023-03-19  发布在  TypeScript
关注(0)|答案(2)|浏览(172)

我有一个简单的歧视联盟,就像这样:

enum Kind {
    A = "a",
    B = "b",
    C = "c"
}

type Value =
    | { kind: Kind.A, value: 1 }
    | { kind: Kind.B, value: 2 }
    | { kind: Kind.C, value: 3 }

我想将中的转换为一个对象类型,其中Kind中的每个键都有一个来自Value["value"]的对应值。对于上面的示例,它应该看起来像{ a: 1, b: 2, c: 3 }
为了从一个有区别的并集中选择一个值,我使用了一个带有具体种类属性的对象的交集:

type t1 = (Value & { kind: Kind.A })["value"]
//   ^? type t1 = 1

但是当我在Map类型中做同样的事情时,它会输出Value["value"]

type t2 = { [K in Kind]: (Value & { kind: K })["value"] }
//   ^? type t2 = { a: 1 | 2 | 3, b: 1 | 2 | 3, c: 1 | 2 | 3 }

我错过了什么?
下面是解决我的问题的可行方案:

type t3 = { [K in Kind]: (Value & { kind: K }) }

// This almost works, the main problem has magically disappeared by doing this in two iterations instead of one
// But now TS says that "value" cannot be used to index t3[K]
type t4 = { [K in Kind]: t3[K]["value"] }

// Actually works
type t5 = { [K in Kind]: t3[K] extends infer T extends { value: unknown } ? T["value"] : never }

还通过先合并交集解决了索引问题:

type Merge<T> = { [K in keyof T]: T[K] }

type t6 = { [K in Kind]: Merge<Value & { kind: K }> }
type t7 = { [K in Kind]: t6[K]["value"] }

我把问题缩小到以下几点:

enum Kind {
    A = "a",
    B = "b",
    C = "c"
}

type Value =
    | { kind: Kind.A, value: 1 }
    | { kind: Kind.B, value: 2 }
    | { kind: Kind.C, value: 3 }

type t1 = Kind.A extends infer K extends Kind.A ? (Value & { kind: K })["value"] : never
//   ^? type t1 = 1 | 2 | 3

type t2 = (Value & { kind: Kind.A })["value"]
//   ^? type t2 = 1

显然,如果Kind. A是一个类型参数(即使它有一个与上例中等效的约束),TS会以不同的方式对待它。
这是为什么呢?
TSPlayground示例

vuktfyat

vuktfyat1#

这是TypeScript的设计限制,如microsoft/TypeScript#45428中所述。
indexing(Value & { kind: K })["value"]这样的交集类型的问题是,编译器根本没有意识到,在计算value属性类型时,它应该参考{kind: K},因为{kind: K}没有value属性。
给定一个(A & B)[P]类型,其中P扩展keyof A,但不扩展keyof B(并且A & B还没有立即求值),编译器会采取捷径,只将其求值为A[P]。这几乎总是正确的做法,但您发现了一种情况,即它不是,或者至少它不是明显正确的。
你觉得{foo: never, bar: 2}["bar"]}应该是什么?看起来应该是2,但是如果有一个规则因为{foo: never, bar: 2}不存在而将其折叠为never呢?那么它可能应该是(never)["bar"],也就是never。它可以是2never,这是有争议的,取决于折叠规则应该在什么时候应用。您的情况与此等效,只是使用了这些类型的并集:({foo: "a", bar: 1) | {foo: never, bar: 2})["bar"])可以是1 | 2或仅是1,这取决于何时应用折叠规则。
但是,即使我们决定它应该是never1,编译器也不会费心去弄清楚这一点,因为这样做会使计算类型的成本一直都更高,只是为了偶尔提高正确性。所以这是TypeScript的一个设计限制。解决方法是使用GitHub问题和该问题的其他答案中描述的Extract实用程序类型。

iugsix8n

iugsix8n2#

类型实用程序Extract<Type, Union>可用于以下情况:
通过从Type提取可分配给Union的所有联合成员来构造类型。
示例

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
//    ^? type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>;
//    ^? type T1 = () => void

它可以像下面这样应用到您的示例中:
TSPlayground

enum Kind {
  A = "a",
  B = "b",
  C = "c",
}

type Value =
  | { kind: Kind.A; value: 1; }
  | { kind: Kind.B; value: 2; }
  | { kind: Kind.C; value: 3; };

type KindValueMap = { [K in Kind]: Extract<Value, { kind: K }>["value"] };
   //^? type KindValueMap = { a: 1; b: 2; c: 3; }

或者,可以从每个联合成员创建Map类型,因为每个成员都具有所需的所有信息:
TSPlayground

type KindValueMap = { [V in Value as V["kind"]]: V["value"] };
   //^? type KindValueMap = { a: 1; b: 2; c: 3; }

相关问题