В чем разница между дженериком, который расширяет тип, и обычным типом в Typescript?

Здравствуйте, специалисты по машинописи.

может кто-нибудь объяснить, почему следующий код дает мне ошибку в строке 16, но не в 13. Это задумано или отсутствует функция?

Код

interface Config {
  // There need to be different types in here for the error to occur
  A: number
  B: string
}

type union = "A" | "B" 

var Global = {A: 1, B: "Hello"} as Config

function foo<K extends union>(key: K, x: union) {
  if (x === "A"){
    Global[x].toFixed()
  }
  if (key === "A"){
    Global[key].toFixed()
  }
}

Playground Link


person Yugon    schedule 07.02.2020    source источник


Ответы (2)


Вероятно, это недостающая функция.

Когда вы отмечаете (x === "A"), он действует как type guard для типа значения x, в результате чего он сужается от типа объединения "A"|"B" до просто "A" через управление анализом потока.

К сожалению, общие параметры типа в TypeScript не могут быть сужены с помощью анализа потока управления; см. microsoft/TypeScript#24085 для получения дополнительной информации. Так, например, проверка (key === "A") не сужает тип K до "A". Такое ограничение имеет смысл, когда у вас есть несколько значений одного и того же универсального типа:

function foo<K extends Union>(key: K, x: Union, key2: K) {    
  if (key2 === "A") {
    Global[key].toFixed(); // error!
  }
}

Очевидно, что проверка значения key2 не должна влиять на тип key, поэтому компилятор консервативно не считает, что K следует сужать. Это основная проблема в Microsoft/TypeScript#13995, и было поднято несколько связанных с ней проблем с предложениями о том, как справиться с ней в случаях, когда такое сужение должно быть безопасным. Однако до сих пор ничего не вошло в язык.


Однако на самом деле это не вся история; можно возразить: хорошо, возможно вы не можете сузить параметр типа K с K extends Union до K extends "A", но конечно вы можете сузить тип значения key от K до "A" или K & "A" (тип пересечения), что сделает Global[key].toFixed() успешным:

if (key === "A") {
  Global[key as (K & "A")].toFixed(); // okay
}

И у меня нет хорошего ответа на это прямо сейчас. Большинство вопросов, которые я видел по этому поводу, в конечном итоге были переданы в microsoft/TypeScript#13995. ????‍♂️

Самое близкое, что я могу прийти к полному ответу, это то, что кажется, что использование встроенных средств защиты типа, таких как a === b или typeof a === "string" или a instanceof B или a in b, заканчивается только фильтрацией объединений или, возможно, сужением string или number до строковых или числовых литералов, но никогда создает тип пересечения. Я спрашивал ранее, см. microsoft/TypeScript#21732, для защиты типа a in b для создания некоторых пересечений, но это не было реализовано. Таким образом, это могут быть две недостающие функции:

  • отсутствие сужения параметров универсального типа и
  • нет ограждения встроенного типа, сужающегося к перекресткам.

Итак, обходные пути: очевидно, что самый простой для этого примера — просто переназначить значение переменной общего типа переменной типа объединения:

const k: Union = key;
if (k === "A") {
  Global[k].toFixed();
}

Или вы можете использовать утверждение типа, как в выше as (K & "A") или просто as "A":

if (key === "A") {
  Global[key as (K & "A")].toFixed(); // okay
  Global[key as "A"].toFixed(); // okay
}

Или, если это происходит часто, вы можете написать свои собственные защита определяемого пользователем типа, так как защита определяемого пользователем типа делает создание пересечений в ветви true последующего потока управления:

const isEq =
  <T extends string | number | boolean>(v: any, c: T): v is T => v === c;

if (isEq(key, "A")) {
  Global[key].toFixed(); // okay, key is now of type K & "A";
}

Playground link to code

person jcalz    schedule 07.02.2020

Я не уверен, почему возникает эта ошибка, но я уверен, что у кого-то есть хорошее объяснение. Вот альтернативный способ добиться того же, используя типы пересечений с дженериками:

interface Config {
  // There need to be different types in here for the error to occur
  A: number
  B: string
}

type Union = keyof Config;

const Global: Config = { A: 1, B: "Hello" };

function foo<K>(key: K & Union, x: Union) { // Use intersection type instead of inheritance
  if (x === "A"){
    Global[x].toFixed();
  }
  if (key === 'A'){
    Global[key].toFixed()
  }
}

Playground link

Также см. этот вопрос: Разница между расширяющимися и пересекающимися интерфейсами в TypeScript?

person smac89    schedule 07.02.2020