为什么基元类型(例如数字)可以分配给对象类型(例如数字),反之亦然?

why primitive types (eg. number) are assignable to object types(e.g. Number) but not vice versa?

提问人:adal 提问时间:12/6/2022 最后编辑:adal 更新时间:12/7/2022 访问量:156

问:

TLDR 版本:

为什么 TypeScript 允许以下内容

let num: Number = new Number(1);
num = 1;

但不允许这样做

let num: number = 1;
num = new Number(1)

众所周知,JS 有两个类似字符串、类似数字的值。

let primNum = 1;
let objNum = new Number(1);

let primString = "str";
let objString = new String("str");

建议大家不要混淆这些,并且非常喜欢原始版本。

TypeScript 是 Javascript 的静态类型超集,它试图最大限度地减少类型错误。如果存在可能会混淆的值,并且该操作会导致 flakey 结果,则 TS 会在编译时显示错误。例如,考虑 和 之间的区别。在 JavaScript 中,第一个值的计算结果为 ,第二个值的计算结果为 。 从 JS 的角度来看,您可以添加任意两个值。您可以毫无问题地添加两个字符串、两个数字、一个数字和一个字符串等。但是,这种行为令人困惑,当 的操作数不是字符串或两个数字时,TypeScript 通过拒绝编译来消除可能的混淆。重要的一点是,即使一些 JS 代码可能期望能够添加任何两个东西,或者这样的期望可能很少见,但没关系,TS 划清了界限,不允许这样的操作。1 + 1 + "1" "1" + 1 + 121111+

然而,TypeScript 对以下几点毫无顾忌,

let num: Number = new Number(1);
num = 1;

首先,我们声明该变量为对象类型(在TypeScript术语a中),但稍后我们给该变量分配一个原始类型(在TS术语a中)。这是一个混淆,应该被 TS 发现,但 TS 允许它,即使没有警告。因此,人们可能会假设 TS 不区分 和 。(我们可能不喜欢不区分它们的决定,但至少在这种情况下,我们知道我们在做什么)numNumbernumberNumbernumber

但这也是不正确的。TS 正确地给出了以下错误:

let num: number = 1;
num = new Number(1)

这意味着可以将 a 分配给 a,但反之亦然?因此,主要问题是,如果 TS 可以区分 a 以及为什么它允许将 a 分配给 .为什么不禁止这两项任务?numberNumbernumberNumberNumbernumber

这是一个错误吗?如果不是,这背后的设计决策是什么?

TS 目前没有规范,但旧的存档说明是这样说的

为了确定子类型、超类型和赋值兼容性关系,Number、Boolean 和 String 基元类型分别被视为具有与“Number”、“Boolean”和“String”接口相同的属性的对象类型。

如果这是真的,我们应该能够将 a 分配给 a(我们可以),但我们也应该能够做相反的事情。这意味着这个旧文档跟不上 TS 目前的工作方式。问题是确定基元和对象类型之间子类型关系的当前实际规则是什么。numberNumber

人们可能不同意这样的规则,因为无论它是什么,它都不能消除 和 之间的混淆,但这不是重点。关键是,目前尚不清楚该规则是什么。numberNumber

主要问题:确定基元类型何时是另一种类型的子类型的规则是什么?

在 StackOverflow 中提出了类似的问题,但所有答案都解释了数字基元类型和数字对象类型之间的区别,并建议避免使用对象版本。但这里的问题不在于 JavaScript 值,而在于 TypeScript 的类型系统。问题是,在什么基础上比较原始类型和对象类型?这个问题不是任何已经回答的问题的重复。

TypeScript 基元 对象类型

评论

0赞 0x150 12/6/2022
想象一下,它就像在延伸.您可以将值赋给类型变量,但尝试将 赋值给类型变量将失败,因为在层次结构中它比纯类型更高intNumberintNumberNumberintintNumber
0赞 adal 12/6/2022
我明白你的意思,但它没有回答问题,只是重复它。我观察到这是可同化的,因为只有当一个是另一个的子类型时,赋值才会成功,这意味着它是 的子类型。但问题是为什么会这样?例如,Java 在原始类型和对象类型(它们称之为盒装类型)之间具有相同的区别,并且两者都不是另一个的子类型。问题是,既然存在很大的问题,为什么 TS 会做出这样的设计选择。答案是否与具有结构与 Java 名义类型的 TS 有任何关系numberNumbernumberNumber
0赞 Alex Wayne 12/6/2022
如果你能把这归结为一个问题,那会是什么?我很难找到这个问题。是“为什么它允许将一个数字分配给一个数字?因为事实并非如此:typescriptlang.org/play?#code/......
0赞 adal 12/6/2022
问题是为什么它允许将 A 分配给numberNumber

答:

2赞 Alex Wayne 12/6/2022 #1

重要提示:

不要使用构造函数。接下来假设你无视这个非常好的建议。Number


我想你问为什么允许这样做:

const bigN: Number = 123 // fine

如果不是:

const smallN: number = new Number() // error

你期望两者都是一个错误。


答案是,您可以将 a 用作 a,而不会注意到其中的区别。但是,如果不改变工作方式,您就不能将 a 用作 a。numberNumberNumbernumbernumber

MDN 上查看有关 Number 构造函数的说明

当作为构造函数(使用 )调用时,它会创建一个对象,该对象不是基元。例如,和(虽然是新的)。NumbernewNumbertypeof new Number(42) === "object"new Number(42) !== 42Number(42) == 42

警告:您很少会发现自己被用作构造函数。Number

例如,请注意:

new Number(42) !== 42

所以:

const n1 = new Number(123)
const n2 = new Number(123)
if (n1 === n2) {
  doSomethingImportant()
  // expected this to run, never does.
}

根据运算符,人们期望两个相同的数字相等,但如果至少有一个是对象,那么情况就不是这样,你的代码可能会在某个地方中断。所以这是值得防范的。===Number

另一方面,假设您确实期望:

const n1: Number = 123
const n2: Number = 123
if (n1 === n2) {
   throw new Error('never expected this to happen!')
   // expected this NOT to run, but it does
}

现在你可以争辩说这同样是错误的。但事情是这样的:

世界上检查数值等效性的代码量大大使期望两个 Number 对象不具有相同标识的代码量相形见绌。

这意味着这在天文数字上不太可能是一个错误。

这就是为什么 是 可分配给 .numberNumber


就像 Typescript 中的许多东西一样,我认为这是纯粹和实用主义之间的权衡。绝大多数情况下,使用 a as 不会造成任何问题,而反过来则至少会破坏一些非常重要的行为,即预期的行为方式。numberNumbernumber

评论

0赞 adal 12/6/2022
我不确定“数字是数字的子类型”的说法是否正确。Number 是 object 类型的子类型。如果 number 是 Number 的子类型,那么 number 也必须是对象的子类型。但是,您不能为对象分配编号。
0赞 Alex Wayne 12/7/2022
也许“这就是为什么可以分配给”会是一个更好的句子,因为层次结构确实并不完全清楚。像这样的恶作剧仍然是允许的:tsplay.dev/WPzpZN。但同样,Typescript 并非在所有情况下都完美无缺。numberNumber