7

Question about TypeScript {} type - up to this point I thought it means "empty object with no properties" type, but recently I stumbled upon eslint rule which forbids using {} type because means "any non nullish value". Quick test in typescript playground says it's true:

let d: {} = {};

d = false;

This code doesn't give compiler error, but when I try to assign null to d, indeed there is an error. So my questions are:

  1. What is actually {} type in TypeScript? Does it really stands for "any non nullish value" (can't find confirmation in TypeScript documentation)?

  2. How should I actually type "empty object without any properties"?

Furman
  • 2,017
  • 3
  • 25
  • 43
  • Check out this answer here https://stackoverflow.com/a/36969975/13058340 - "The type object {} represents a 0-field object. The only legal value of this type is an empty object: {}." – Pedro Filipe Jun 15 '20 at 21:55
  • @PedroFilipe While that _seems_ to answer the question, and is a nice find, it doesn't explain why the code sample compiles without errors. – Etheryte Jun 15 '20 at 22:00
  • When you are reassigning d to false, this is a valid operation because `let` lets you do this. You effectively saying - make the reference to this empty object disappear and replace it with a false value – Pedro Filipe Jun 15 '20 at 22:01
  • I don't understand, I explicity told compiler that d is {} type, so it shouldn't let me assign boolean value. Note that assigning null gives compiler error. – Furman Jun 15 '20 at 22:05
  • @PedroFilipe That's not the case, it doesn't work if you do it the other way around: `let d: boolean = false; d = {};`. That will give a compiler error as it should. – Etheryte Jun 15 '20 at 22:16
  • Ah ok ok. Could it be a bug with Typescript? – Pedro Filipe Jun 15 '20 at 22:19
  • The compiler error may come from the --strictNullChecks option which removes null and undefined from the domain of types – Raphaël Michel Jun 16 '20 at 12:31
  • Does this answer your question? [What does {} mean in TypeScript?](https://stackoverflow.com/questions/60381643/what-does-mean-in-typescript) – Bart Louwers Aug 11 '22 at 20:06

5 Answers5

6

1. What is actually {} type in TypeScript? Does it really stand for "any non-nullish value" (I can't find confirmation in TypeScript documentation)?

If you do console.log(typeof(d)); then you will see that {} is of type object. This is not entirely correct but let me explain about object first. First of all object with lowercase o can be any non-primitive value while Object with uppercase O can include any primitive value through Object.prototype.

So if you try to overwrite object with a primitive value it will give errors as it doesn't like primitive values, although null will work as is an object type as well, undefined on the other hand is of type undefined but this can always be assigned.

Now {} is called "Object literal" this is actually both object and Object. So that is why both primitive and non-primitive values are assignable to an object literal as mentioned on packt.

So normally any value can be assigned to an object literal, unless strictNullChecks is enabled as null and undefined cannot be assigned in this case as @RobbieSpeed mentioned in the comments.

You can check this in the following stackblitz, beware that strictNullChecks is not enabled here.

2. How should I actually type "empty object without any properties"?

There are probably a couple of ways to do this, the ones I know are you can either instantiate it like you did or do: let d = {}; as the type is automatically determined there is no need to add a type.

The other method is to use an interface to define your properties when they are known but make them all optional by adding question marks behind the property names. This makes it easy to use aswell since all your properties are known and can be found by intellisense aswell.

Example:

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

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

If this didn't answer your question sufficiently, feel free to ask away!

Ps: for more information on objects check out 2ality

Update #1

As Paddokt mentioned in the comments if you want to type an object either as an empty object or only a specific object the above example won't work so to do this a different approach is required.

If you want the example above to be only either a User or an empty object you would have to wrap the user object inside a different object as following:

export interface optUser {
    user?: User;
}

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

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

This way you can have a variable that is either an empty object or an object that contains a user where both name and email are required.

Note:

Do know that optuser.user = {}; won't work, either optuser has a user object or it doesn't have an object at all, as User itself can't be an empty object here.

Billy Cottrell
  • 443
  • 5
  • 20
  • If you want to type the result or input of a function, for example, you have to explicitly type your object. So if it's either `MyInterface` or an empty object, a different solution is needed. The second solution isn't right either, because TS would consider `user: User = { email: 'a@b.com' }` to be right whereas you'd probably want both properties or none. – paddotk Sep 07 '21 at 16:15
  • 1
    What you are saying is indeed correct, if you want to have an object that is either `MyInterface` or an empty object you would have to use a different solution. Which is wrapping the `MyInterface` inside another object which would look like this: `export interface optUser { user?: User; }` `export interface User { name: string; email: string; }` When an object is written like this, optUser will either accept an empty object or an object containing a user where both properties are required. I'll update the answer correspondingly. – Billy Cottrell Sep 08 '21 at 07:07
  • "So normally any value can be assigned to an object literal." That's not true the only reason stackblitz is allowing undefined and null to be assigned is because it does not have strictNullChecks enabled. Here is a playground [link](https://www.typescriptlang.org/play?strict=false&noImplicitAny=false&strictFunctionTypes=false&strictBindCallApply=false&ts=4.0.5#code/DYUwLgBAhgXBDeBfCBeCA7ArsYBuIA) – Robbie Speed Apr 12 '23 at 02:12
  • @RobbieSpeed you are correct if strictNullChecks is enabled you cannot assign null or undefined! I'll update my answer! – Billy Cottrell Apr 12 '23 at 08:34
4

The TypeScript FAQ already mentions this:
https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-all-types-assignable-to-empty-interfaces

The type {} has no properties, booleans have the same thing: no properties.

See the logic now?

One can actually try to look up a property on a boolean/number.

Here's the code that you would expect:

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

Which would be undefined and false for the two expressions respectively.

Yet, let's do something stupid, we'll swap it in with a number and a boolean:

0.0.a;
'a' in 0.0;

true.a;
'a' in true;

They still give the same results, there would be no difference.

Thus, they could fit into wherever {} would, since they won't throw, and they will return undefined for all of the required properties.

Now take these:

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

And you have runtime errors, thus they cannot mimic an empty object {};


Because of what I gave above... it isn't possible to get an empty object.

The null prototype issue hasn't been solved yet, thus we have no type for this expression: Object.create(null).

Type inference will still result in the same problem:

let x = {};

x is deducted as {}, not helpful.

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

Valid, thus a fail for us.

It seems there is no solution for this.

3

It's the newest Typescript declaration standard.

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

d.whatever = 'hello';
gatsbyz
  • 1,057
  • 1
  • 10
  • 26
2

TypeScript checks that objects have all the properties their type declares. However, TypeScript doesn't generally require that types declare all properties an object has.

For instance, if we declare:

let person: {name: string}; 

we may do

let joe = {name: 'joe', occupation: 'developer'};
person = joe; // just fine: joe has everything a person needs

and likewise, we can do:

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

That is, the type {} can indeed be assigned any object, because it doesn't require that object to have any particular properties.

That leaves the puzzling question of how false, a boolean, could possibly pass for an object. The reason is:

For purposes of determining type relationships (section 3.11) and accessing properties (section 4.13), the Boolean primitive type behaves as an object type with the same properties as the global interface type 'Boolean'.

That is, because EcmaScript will automatically convert a boolean into a Boolean if required, TypeScript allows passing a boolean whenever a Boolean is allowed (and Boolean is allowed because it has all properties {} needs ...)

So yes, it is correct that everything except null and undefined could be put into a variable of type {}. That's by design. {} doesn't require any properties, so any object will do.

How should I actually type "empty object without any properties"?

If you mean to say: This is some object, but I don't need it to have any particular properties (for instance because you won't be accessing any properties, or will be accessing them in a generic way), using {} or object is fine, though object would probably convey your intent more clearly.

If you mean to say: This object will never have any properties ... you can't. TypeScript can not ensure this, because it must interoperate with JavaScript, which allows adding properties to objects at any time:

const empty = {};

// somewhere else
empty['foo'] = 'bar'; // no longer empty :-)
meriton
  • 68,356
  • 14
  • 108
  • 175
2
type EmptyObject = Record<string | number | symbol, never>

Answer updated based on Martin Geisse's feedback

Robert Monfera
  • 1,980
  • 1
  • 22
  • 16
  • 1
    I think this is wrong. Record says that "for none of the properties (all of type never), it is known that their type can be anything (type unknown)", and indeed didn't pevent me from assigning any properties to an instance of the type. What worked is Record, that is, "for all properties of type string|number (all of them), all values of type never (which don't exist) are appropriate". – Martin Geisse Oct 13 '22 at 11:25