打字稿:无法提取带有标签类型保护的对象的值类型

Typescript: can't extract value type of object with labeled type guard

我不知道为什么无法使用泛型正确提取对象值类型,如下所示。 如果你知道,能告诉我为什么它不起作用吗?

export type ValueType = {
    type: "string",
    value: string,
} | {
    type: "number",
    value: number,
}

/** 1. Extract value type of "string" type */
type StringValueType = (ValueType & {type: "string"})["value"];
// type StringValueType = string

/** 2. Extract value type of each type by generic */
type ValueTypeWithGeneric<T extends ValueType["type"]> = (ValueType & {type: T})["value"];
type StringValueTypeWithGeneric = ValueTypeWithGeneric<"string">;
// type StringValueTypeWithGeneric = string | number
// I expect this type to be string

这是 TypeScript 的设计限制;请参阅 microsoft/TypeScript#45428 以获得权​​威答案。当遇到如下形式的类型时

type X<T> = ({ a: 0, b: "x" } & { a: T })["b"] 

编译器假定这将始终等同于

type X<T> = ({ a: 0 & T, b: "x" })["b"]

无论 generic 类型参数 T 是什么。因此编译器急切地将其缩减为

type X<T> = "x"

所以 X<T> 只是 "x" 独立于 T:

type XT0 = X<0> // "x" 
type XT1 = X<1> // "x" 

但是当您实际使用 non-generic 类型代替 T 执行此操作时,您会看到不同的行为:

type X0 = ({ a: 0, b: "x" } & { a: 0 })["b"] // "x"
type X1 = ({ a: 0, b: "x" } & { a: 1 })["b"] // never 

这是X<1>X1之间的差异运行。


在 TypeScript 3.8 及更低版本中, 没有差异。输入 X1 也将计算为 "x"

// TS3.8 and below
type X1 = ({ a: 0, b: "x" } & { a: 1 })["b"] // "x" in TS3.8-

即使 a 属性 的值不可能同时为 "0""1" 类型,编译器仍保留 {a: 0, b: "x"} & {a: 1} as-is,因此 b 属性 被视为 "x".

类型

但是 TypeScript 3.9 引入了对 reducing intersections by discriminant properties, as implemented in microsoft/TypeScript#. Now the compiler sees {a: 0, b: "x"} & {a: 1} as the never type 的支持,因此 b 属性 也是 never 类型。

这太棒了。但不幸的是,面对泛型的行为没有更新以说明这一变化。当与泛型 {a: T} 相交时,无论如何都不会发生缩减。那好吧。这是设计限制。


此处推荐的解决方法是避免为此目的使用交集,而是使用 the Extract<T, U> utility type,其目的是将联合类型 T 过滤为仅可分配给 U 的成员.因此,您可以编写 Extract<{a: 0, b: "x"}, {a: T}> 而不是 {a: 0, b: "x"} & {a: T} 并获得一致的行为:

type Ex<T> = Extract<{ a: 0, b: "x" }, { a: T }>["b"] 
type Ex0 = Ex<0> // "x" 
type Ex1 = Ex<1> // never 

这意味着您的示例也将开始按预期运行:

/** 1. Extract value type of "string" type */
type StringValueType = Extract<ValueType, { type: "string" }>["value"];
// type StringValueType = string 

/** 2. Extract value type of each type by generic */
type ValueTypeWithGeneric<T extends ValueType["type"]> = Extract<ValueType, { type: T }>["value"];
type StringValueTypeWithGeneric = ValueTypeWithGeneric<"string">;
// type StringValueTypeWithGeneric = string 

Playground link to code in TS4.6

Playground link to code in TS3.8