为什么 TypeScript 在对象字面量赋值时将字符串字面量联合类型转换为字符串?

5
我是一名有用的助手,可以为您翻译文本。
我喜欢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' 类型。

似乎唯一让它按照我的预期行事的方法是将 'foo' 强制转换为 Status,如下所示:

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

在这种情况下,打字是正常工作的。我对此感到满意。

3个回答

13
编译器使用一些启发式方法来确定何时扩大字面量。其中之一是以下内容:
  • 对象字面量中属性的推断类型是表达式的扩展文字类型,除非属性具有包含文字类型的上下文类型。
因此,默认情况下,在分配给foobar的对象字面量中,"foo" | "bar"会被扩展为string

TS 3.4更新

现在你可以使用const断言来请求更窄的类型:

const foobar = {
    bar
} as const;
/* const foobar: {
    readonly bar: "foo" | "bar";
} */

在本答案的其余部分中,{{literally()}}函数可能仍然有些用处,但我建议尽可能使用{{as const}}。
注意启发式算法的一部分,即“除非属性具有包含文字类型的上下文类型”。向编译器暗示像"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()这样的东西,我希望这能帮助你;祝你好运!


3
那是因为TypeScript对待letconst的方式不同。常量始终被视为“较窄”的类型-在这种情况下是字面量。变量(以及非只读对象属性)则被视为“扩展”类型-string。这是伴随文字类型的一条规则。
现在,虽然您的第二个赋值可能是一个常量,但该常量的属性实际上是可变的-它是一个非只读属性。如果您没有提供“上下文类型”,则狭义推断会丢失,并且您将获得更宽泛的string类型。
在此处阅读有关文字类型的更多信息。我可以引用:
引用:

未带类型注释的const变量或readonly属性的推断类型是初始化程序的类型。

带有初始化程序但没有类型注释的let变量、var变量、parameter非只读属性的推断类型是初始化程序的扩展文字类型。

并且更清晰地说明:
引用:

对象文字中属性的推断类型是表达式的扩展文字类型,除非该属性具有包括文字类型的上下文类型。

顺便说一句,如果您为常量提供上下文类型,则该类型将传递给变量:
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'

3

针对更新的问题,使用三值文字联合类型进行回答:

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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接