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

Zir*_*iax 8 typescript typescript-generics

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

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" };
Run Code Online (Sandbox Code Playgroud)

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

所以

interface TypedValue<T = ValueType, D = TypeOf<T>> 
Run Code Online (Sandbox Code Playgroud)

对应于

interface TypedValue<"NUM", number> 
interface TypedValue<"NUM", string> 
interface TypedValue<"NUM", never> 
interface TypedValue<"STR", number> 
interface TypedValue<"STR", string> 
interface TypedValue<"STR", never> 
Run Code Online (Sandbox Code Playgroud)

也许更多,虽然我实际上希望这个泛型类型只对应于

interface TypedValue<"NUM", number> 
interface TypedValue<"STR", string> 
Run Code Online (Sandbox Code Playgroud)

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

我知道使用抑制类型分布的技巧

type TypeOf<T>
    = [T] extends ["NUM"] ? number
    : [T] extends ["STR"] ? string
    : never;
Run Code Online (Sandbox Code Playgroud)

但我似乎无法自己解决这个难题,我真的很想深入研究这个神奇的类型系统,所以欢迎任何帮助:) 很确定 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"]
Run Code Online (Sandbox Code Playgroud)

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

无限感激,我已经为此苦苦挣扎了很长时间。

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

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

Tit*_*mir 5

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

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

由于这是变量的类型,对象文字将仅根据类型进行检查并与之兼容(typeis "NUM"、 compatible with "NUM" | "STR"datais of type stringcompatible with string | 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" };
Run Code Online (Sandbox Code Playgroud)

上面TypedValue没有类型参数的定义等效于:

{
    type: "NUM";
    data: number;
} | {
    type: "STR";
    data: string;
}
Run Code Online (Sandbox Code Playgroud)

这意味着 a typetoSTR永远不能与 a dataof type兼容,numbera typetoNUM永远不能与 a dataof type兼容string

条件类型 inTypedValue不用于表达实际条件, every Twill extend any。条件类型的要点是分布在T. 这意味着如果T是联合,则结果将是{ type: T; data: TypeOf<T>; }应用于联合的每个成员的类型。在此处阅读有关条件类型的分配行为的更多信息

  • @Ziriax 在最后添加了一个解释,我们只是使用了我们实际上并不关心条件的条件类型的分配部分。 (2认同)