TypeScript中的“{}”类型

8
关于TypeScript的{}类型的问题 - 直到现在我认为它表示“没有属性的空对象”类型,但最近我遇到了一个eslint规则,禁止使用{}类型,因为它表示“任何非nullish值”。 在typescript playground上进行快速测试发现这是真的。
let d: {} = {};

d = false;

这段代码没有编译错误,但是当我尝试将 null 赋值给 d 时,确实会出现错误。所以我的问题是:

  1. 在 TypeScript 中,实际上什么是 {} 类型?它是否真的代表“任何非空值”(无法在 TypeScript 文档中找到确认)?

  2. 我应该如何为“没有任何属性的空对象”打字?


请查看此处的答案 https://dev59.com/VFoU5IYBdhLWcg3w555Q#36969975 - “类型对象 {} 表示一个零字段对象。此类型的唯一合法值是一个空对象:{}。” - Pedro Filipe
虽然这似乎回答了问题,而且是个好发现,但它并没有解释为什么代码示例可以编译而没有错误。 - Etheryte
当你将d重新赋值为false时,这是一个有效的操作,因为let允许你这样做。实际上,你是在说——让对这个空对象的引用消失,并用false值替换它。 - Pedro Filipe
我不明白,我明确告诉编译器d是{}类型,所以它不应该让我分配布尔值。请注意,分配null会导致编译器错误。 - Furman
这个回答解决了你的问题吗?TypeScript中的{}是什么意思? - Bart Louwers
显示剩余3条评论
5个回答

6

1. TypeScript 中的 {} 类型实际上是什么?它真的代表“任何非空值”吗(我在 TypeScript 文档中找不到确认)?

如果你执行 console.log(typeof(d));,那么你会发现 {} 的类型是 object。这并不完全正确,但让我先解释一下关于 object 的问题。首先,小写字母 oobject 可以是任何非原始值,而大写字母 OObject 可以通过 Object.prototype 包括任何原始值。

因此,如果你尝试用原始值覆盖 object,它会报错,因为它不喜欢原始值,虽然 null 作为一个 object 类型也可以工作,但是 undefined 是 undefined 类型,但是这总是可以被赋值。

现在{}被称为“对象字面量”,实际上它是objectObject的组合。因此,正如packt所提到的那样,原始值和非原始值都可以分配给对象字面量。

因此,通常任何值都可以分配给对象字面量,除非启用了strictNullChecks,则在这种情况下无法分配null和undefined,如@RobbieSpeed在评论中提到的。

您可以在以下stackblitz中检查此内容,请注意此处未启用strictNullChecks。

2. 实际上应该如何输入“没有任何属性的空对象”?

有几种方法可以做到这一点,我知道的方法是您可以像您所做的那样进行实例化,或者执行let d = {};,因为类型会自动确定,因此不需要添加类型。

另一种方法是使用接口来定义属性,当它们已知时,通过在属性名称后面添加问号使它们全部变为可选。 这样做不仅易于使用,而且所有属性都是已知的,并且可以通过智能感知找到。
示例:
export interface User {
    name?: string;
    email?: string;
}

let user: User = {};
user.name = "Joe";
user.email = "joe@hotmail.com";

如果这个回答没有充分解决您的问题,请随时提问!

附注:有关对象的更多信息,请查看2ality

更新 #1

正如Paddokt在评论中提到的,如果您想将对象作为空对象或仅特定对象进行输入,则上面的示例将无法工作,因此需要采用不同的方法。

如果您希望上面的示例仅为User或空对象,则必须将用户对象包装在另一个对象中,如下所示:

export interface optUser {
    user?: User;
}

export interface User {
    name: string;
    email: string;
}

let optuser: optUser = {};
optuser.user = {
  name: "Joe",
  email: "joe@hotmail.com"
}

这样你就可以拥有一个变量,它可以是一个空对象,或者一个包含用户的对象,其中用户名和电子邮件地址都是必需的。
注意: 请注意,optuser.user = {}; 不起作用,要么 optuser 有一个用户对象,要么根本没有对象,因为在此处User本身不能是一个空对象。

如果你想输入或输出一个函数的结果,例如,你必须明确地输入你的对象。因此,如果它是MyInterface或者是一个空对象,需要不同的解决方案。第二种解决方案也不正确,因为TS会认为user: User = { email: 'a@b.com' }是正确的,而你可能希望有两个属性或没有。 - paddotk
1
你所说的确实正确,如果你想要一个既是 MyInterface 又是空对象的对象,你需要使用另一种解决方案。这个方案是将 MyInterface 包装在另一个对象中,看起来像这样: export interface optUser { user?: User; } export interface User { name: string; email: string; } 当一个对象被写成这样时,optUser 将接受一个空对象或一个包含用户的对象,其中两个属性都是必需的。我会相应地更新答案。 - Billy Cottrell
所以通常任何值都可以分配给对象字面量。这并不正确,Stackblitz允许分配undefined和null的唯一原因是它没有启用strictNullChecks。这里有一个playground链接(https://www.typescriptlang.org/play?strict=false&noImplicitAny=false&strictFunctionTypes=false&strictBindCallApply=false&ts=4.0.5#code/DYUwLgBAhgXBDeBfCBeCA7ArsYBuIA)。 - Robbie Speed
@RobbieSpeed,你是正确的,如果启用了strictNullChecks,就不能赋值为null或undefined!我会更新我的答案! - Billy Cottrell

4

TypeScript FAQ 已经提到了这个问题:
https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-all-types-assignable-to-empty-interfaces

类型 {} 没有属性,布尔类型同样也没有属性。

现在明白逻辑了吗?

实际上,你可以尝试在布尔类型/数字类型上查找属性。

以下是您预期的代码:

{}.a;
'a' in {};

分别对应于两个表达式的结果是 undefinedfalse

然而,让我们做一些愚蠢的事情,我们将其替换为数字和布尔值:

0.0.a;
'a' in 0.0;

true.a;
'a' in true;

它们仍然会得到相同的结果,没有区别。

因此,它们可以适应任何{}所在的位置,因为它们不会抛出异常,并且对于所有必需的属性都将返回未定义。

现在看看这些:

null.a;
undefined.a;
'a' in null;

你遇到了运行时错误,因此无法模仿一个空对象{}


因为我之前所说的...无法获取一个空对象。

空原型问题尚未解决,因此我们没有类型来表示这个表达式:Object.create(null)

类型推断仍然会导致相同的问题:

let x = {};

x被扣除为{},没有用。

const x: Record<never, never> = true;

这是有效的,所以对我们来说是失败的。

看起来对于这个问题似乎没有解决方案。


3

这是最新的TypeScript声明标准。

let d: Record<string, unknown>; // will default to {} value

d.whatever = 'hello';

2
type EmptyObject = Record<string | number | symbol, never>

根据Martin Geisse的反馈更新答案


1
我认为这是错误的。Record<never, unknown> 表示“对于所有类型为 never 的属性,它们的类型都未知(类型为 unknown)”,并且确实没有阻止我将任何属性分配给该类型的实例。有效的方法是使用 Record<string|number, never>,即“对于所有类型为 string|number 的属性(全部),所有类型为 never 的值(不存在)都是适当的”。 - Martin Geisse

2

TypeScript会检查对象是否具有其类型声明的所有属性。但是,TypeScript通常不要求类型声明对象具有所有属性。

例如,如果我们声明:

let person: {name: string}; 

我们可能会做
let joe = {name: 'joe', occupation: 'developer'};
person = joe; // just fine: joe has everything a person needs

同样地,我们也可以做到:

let obj: {} = joe; // just fine: joe has everything obj needs

也就是说,类型{}确实可以被赋予任何对象,因为它不需要该对象具有任何特定属性。
这就留下了一个令人困惑的问题,布尔值false如何可能被视为一个对象。原因在于:为了确定类型关系(第3.11节)和访问属性(第4.13节),布尔原始类型表现为具有与全局接口类型“Boolean”相同属性的对象类型。 也就是说,因为EcmaScript会自动将布尔值转换为Boolean(如果需要),所以TypeScript允许在允许Boolean的任何地方传递boolean(并且允许Boolean,因为它具有{}所需的所有属性...)
因此,除了nullundefined之外,一切都可以放入类型为{}的变量中。这是设计上的。 {}不需要任何属性,因此任何对象都可以。
如果您的意思是:这是某个对象,但我不需要它具有任何特定属性(例如,因为您不会访问任何属性,或者将以通用方式访问它们),使用{}object都可以,尽管object可能更清楚地传达了您的意图。
如果您的意思是:这个对象永远不会有任何属性...则无法确定。 TypeScript无法确保这一点,因为它必须与允许在任何时间向对象添加属性的JavaScript进行交互:
const empty = {};

// somewhere else
empty['foo'] = 'bar'; // no longer empty :-)

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