почему TypeScript преобразует строковый литерал типа объединения в строковый при присваивании в литерале объекта? - PullRequest
0 голосов
/ 23 января 2019

Я люблю строковые литеральные типы объединения в TypeScript.Я натолкнулся на простой случай, когда ожидал сохранения типа объединения.

Вот простая версия:

let foo = false;
const bar = foo ? 'foo' : 'bar';

const foobar = {
    bar
}

bar правильно напечатано как 'foo' | 'bar':

enter image description here

Но foobar.bar набирается как string:

enter image description here

Просто любопытно, почему.

Обновление

Так что @jcalz и @ggradnig делают хорошие моменты.Но потом я понял, что в моем сценарии использования есть дополнительный поворот:

type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';

const foobar = {
    bar
}

Интересно, что bar имеет тип Status.Тем не менее, foobar.bar имеет тип 'foo' | 'bar' still.

Кажется, что единственный способ заставить его вести себя так, как я ожидал, - привести 'foo' к Status, например:

const bar = foo ? 'foo' as Status : 'bar';

В этом случае набор текста работает правильно.Я в порядке с этим.

Ответы [ 3 ]

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

Компилятор использует некоторые эвристики для определения , когда расширять литералы .Одним из них является следующее:

  • Тип, выведенный для свойства в литерале объекта, является расширенным литеральным типом выражения, если свойство не имеет контекстного типа, включающего литеральные типы.

Таким образом, по умолчанию "foo" | "bar" расширяется до string внутри литерала объекта, назначенного для foobar.

Обратите внимание на часть, которая говорит "если свойство не имеет контекстного типа, включающего литеральные типы. "Один из способов подсказать компилятору, что тип, такой как "foo" | "bar", должен оставаться суженным, это заставить его соответствовать типу с ограничением до string (или объединению, содержащему его).Ниже приводится вспомогательная функция, которую я иногда использую для этого:

type Narrowable = string | number | boolean | symbol | object |
  null | undefined | void | ((...args: any[]) => any) | {};

const literally = <
  T extends V | Array<V | T> | { [k: string]: V | T },
  V extends Narrowable
>(t: T) => t;

Функция literally() просто возвращает свой аргумент, но тип имеет тенденцию быть уже.Да, это уродливо ... Я держу это в библиотеке утилит вне поля зрения.

Теперь вы можете сделать:

const foobar = literally({
  bar
});

и тип выводится как { bar: "foo" | "bar" }, как вы ожидали,

Независимо от того, используете ли вы что-то вроде literally(), я надеюсь, это вам поможет;удачи!

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

Чтобы ответить на обновленный вопрос с использованием трехзначного литерального типа объединения:

type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';

Объявленный тип bar равен Status, но его предполагаемый тип все еще сужается анализом потока управления только до двух возможных значений.из трех 'foo' | 'bar'.

Если вы объявите другую переменную без типа, TypeScript будет использовать предполагаемый тип для bar, а не объявленный тип:

const zoo = bar; // const zoo: "foo" | "bar"

Без обращения кутверждение типа as Status, нет способа отключить вывод типа на основе анализа потока управления, кроме явного объявления типа в том месте, где он вам нужен:

const foobar: {bar: Status} = {
    bar // has Status type now
}
0 голосов
/ 23 января 2019

Это потому, что let и const обрабатываются TypeScript по-разному.Константы всегда обрабатываются с более узким типом - в данном случае литералами.Переменные (и свойства объекта, не доступные только для чтения) обрабатываются с расширенным типом - string.Это правило, которое идет вместе с литеральными типами.

Теперь, хотя ваше второе присваивание может быть константой, свойство этой константы фактически изменчиво - это свойство, не предназначенное только для чтения.Если вы не предоставите «контекстный тип», узкий вывод теряется, и вы получаете более широкий string тип.

Здесь вы можете прочитать больше о литеральных типах .Я могу процитировать:

Тип, выведенный для переменной const или свойства только для чтения без аннотации типа, является типом инициализатора как есть.

Тип, выведенный для let переменная, var переменная, parameter или non-readonly property с инициализатором и без аннотации типа - это расширенный литеральный тип инициализатора.

И даже болееясно:

Тип, выведенный для свойства в литерале объекта, является расширенным литеральным типом выражения, если свойство не имеет контекстного типа, включающего литеральные типы.

Кстати, если вы предоставите контекстный тип для константы, тип будет передан переменной:

const bar = foo ? 'foo' : 'bar';
let xyz = bar // xyz will be string

const bar: 'foo' | 'bar' = foo ? 'foo' : 'bar';
let xyz = bar // xyz will be 'foo' | 'bar'
...