我有一个简单的歧视联盟,就像这样:
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示例
2条答案
按热度按时间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
。它可以是2
或never
,这是有争议的,取决于折叠规则应该在什么时候应用。您的情况与此等效,只是使用了这些类型的并集:({foo: "a", bar: 1) | {foo: never, bar: 2})["bar"])
可以是1 | 2
或仅是1
,这取决于何时应用折叠规则。但是,即使我们决定它应该是
never
或1
,编译器也不会费心去弄清楚这一点,因为这样做会使计算类型的成本一直都更高,只是为了偶尔提高正确性。所以这是TypeScript的一个设计限制。解决方法是使用GitHub问题和该问题的其他答案中描述的Extract
实用程序类型。iugsix8n2#
类型实用程序
Extract<Type, Union>
可用于以下情况:通过从
Type
提取可分配给Union
的所有联合成员来构造类型。示例
它可以像下面这样应用到您的示例中:
TSPlayground
或者,可以从每个联合成员创建Map类型,因为每个成员都具有所需的所有信息:
TSPlayground