Как избежать распределения по нескольким параметрам универсального типа в Typescript? - PullRequest
0 голосов
/ 09 января 2019

Я не знал, как правильно сформулировать свой вопрос, поэтому приведу пример.

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 знает, как справиться с этим;)

РЕДАКТИРОВАТЬ Это наконец щелкнуло после ответа Тициана Черникова-Драгомир! Лично я лучше понимаю решение с помощью следующего фрагмента кода:

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 проверит T extends (infer X) для каждого члена объединения "A"|"B", который всегда выполняется успешно, но теперь он связывает переменную соответствующего типа с переменной типа non-union X. И infer X на самом деле не нужен, но это помогло мне лучше понять его.

Бесконечная благодарность, я давно с этим борюсь.

Итак, теперь я наконец понимаю следующую выдержку из руководства Typescript:

В экземплярах распределительного условного типа T extends U ? X : Y ссылки на T в условном типе разрешаются до отдельных составляющих типа объединения (т. Е. T относится к отдельному человеку составляющие после условного типа распределяются по типу объединения). Кроме того, ссылки на T в X имеют дополнительное ограничение параметра типа U (т. Е. T считается присваиваемым U в X).

1 Ответ

0 голосов
/ 09 января 2019

Проблема не столько в условной части вашего решения, сколько в том, как аннотации типов переменных работают в сочетании с параметрами типов по умолчанию.

Если для переменной не указан тип, будет выведен ее тип. Если вы укажете тип, никакого вывода не произойдет. Поэтому, когда вы говорите const test3: TypedValue, никакого вывода для параметра универсального типа не произойдет, и будет использовано значение по умолчанию для параметра типа. Так что const test3: TypedValue эквивалентно const test3: TypedValue<"NUM" | "STR">, что эквивалентно const test3: { type: "NUM" | "STR"; data: number | string; }

Так как это тип переменной, литерал объекта будет просто проверяться по типу, и он совместим с ним (type равен "NUM", совместим с "NUM" | "STR", data имеет тип string совместим с 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" };

С приведенным выше определением TypedValue без параметра типа эквивалентно:

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

Это означает, что type до STR никогда не могут быть совместимы с data типа number, а type до NUM никогда не могут быть совместимы с data типа string.

Условный тип в TypedValue не используется для выражения фактического условия, каждый T будет расширяться any. Смысл условного типа состоит в том, чтобы распределить по T. Это означает, что если T является объединением, результатом будет тип { type: T; data: TypeOf<T>; }, применяемый к каждому члену объединения. Подробнее о распределительном поведении условных типов здесь

...