如何避免在 Typescript 中分布多个泛型类型参数?

How to avoid distribution over multiple generic type parameters in Typescript?

我不知道如何正确地表述我的问题,所以我举个例子。

type ValueType = "NUM" | "STR";

type TypeOf<T>
    = T extends "NUM" ? number
    : T extends "STR" ? string
    : never;

interface TypedValue<T = ValueType> {
    type: T;
    data: TypeOf<T>;
}


// Compiles, as intended
const test1: TypedValue = { type: "NUM", data: 123 };

// Does not compile, as intended
const test2: TypedValue<"NUM"> = { type: "NUM", data: "123" };

// Should not compile, but does...
const test3: TypedValue = { type: "NUM", data: "123" };

Typescript 似乎为接口生成了许多具体类型 TypedValue:

所以

interface TypedValue<T = ValueType, D = TypeOf<T>> 

对应

interface TypedValue<"NUM", number> 
interface TypedValue<"NUM", string> 
interface TypedValue<"NUM", never> 
interface TypedValue<"STR", number> 
interface TypedValue<"STR", string> 
interface TypedValue<"STR", never> 

也许更多,而我实际上希望这个泛型对应于

interface TypedValue<"NUM", number> 
interface TypedValue<"STR", string> 

我怎样才能避免这种类型的分布,例如如何将一个类型参数绑定到打字稿中的另一个类型参数?

我知道使用

抑制类型分布的技巧
type TypeOf<T>
    = [T] extends ["NUM"] ? number
    : [T] extends ["STR"] ? string
    : never;

但我自己似乎无法解决这个难题,我真的很想深入挖掘这个神奇的类型系统,所以欢迎任何帮助:) 很确定 jcalz 知道如何解决这个问题;)

编辑 Titian Cernicova-Dragomir 回答后终于点击了!我个人通过以下代码片段更好地理解了解决方案:

type Pairs1<T> = [T, T];
type Pairs2<T> = T extends (infer X) ? [X, X] : never;

type P1 = Pairs1<"A" | "B">; // => ["A" | "B", "A" | "B"]  
type P2 = Pairs2<"A" | "B">; // => ["A", "A"] | ["B" | "B"]

似乎发生了什么,Typescript 编译器将为每个联合成员 "A"|"B" 检查 T extends (infer X),这总是成功的,但它现在将匹配的类型变量绑定到 non -union类型变量Xinfer X 实际上不是必需的,但它帮助我更好地理解它。

感激不尽,纠结了好久

所以现在我终于理解了 Typescript 手册中的以下摘录:

在分布式条件类型 T extends U ? X : Y 的实例化中,条件类型中对 T 的引用被解析为 联合类型的各个组成部分(即T指的是条件类型分布在联合类型上之后的各个成分)。此外,X 中对 T 的引用有一个额外的类型参数约束 U(即 T 被认为可分配给 X 中的 U)。

问题不在于解决方案的条件部分,而在于变量类型注释与默认类型参数的工作方式。

如果没有为变量指定类型,则将推断其类型。如果指定类型,则不会发生推断。因此,当您说 const test3: TypedValue 时,不会发生对泛型类型参数的推断,并且会使用类型参数的默认值。所以 const test3: TypedValue 等价于 const test3: TypedValue<"NUM" | "STR"> 等价于 const test3: { type: "NUM" | "STR"; data: number | string; }

因为这是变量的类型,对象字面量将只根据类型进行检查并与其兼容(type"NUM",与 "NUM" | "STR" 兼容, data 的类型 stringstring | number)

兼容

您可以完全使用分配行为或条件类型将您的类型转换为真正的可区分联合:

type ValueType = "NUM" | "STR";

type TypeOf<T>
    = T extends "NUM" ? number
    : T extends "STR" ? string
    : never;

type TypedValue<T = ValueType> = T extends any ? {
    type: T;
    data: TypeOf<T>;
}: never;

// Compiles, as intended
const test1: TypedValue = { type: "NUM", data: 123 }; 

// Does not compile, as intended
const test2: TypedValue<"NUM"> = { type: "NUM", data: "123" };

// does not compile now
const test3: TypedValue = { type: "NUM", data: "123" };

根据上面的定义 TypedValue 没有类型参数等同于:

{
    type: "NUM";
    data: number;
} | {
    type: "STR";
    data: string;
}

这意味着 typeSTR 永远不能与 number 类型的 datatypeNUM 永远无法与 string.

类型的 data 兼容

TypedValue中的条件类型不是用来表达实际条件的,每个T都会扩展any。条件类型的要点是分布在 T 上。这意味着如果 T 是一个联合,结果将是应用于联合的每个成员的类型 { type: T; data: TypeOf<T>; }。详细了解条件类型的分配行为 here