All Articles

The Empty Object Type in TypeScript

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

Quick Explanation

  • The empty object type - {} - doesn't behave how you expect in TypeScript.

  • Instead of representing an empty object, it represents any value except null and undefined.

  • This is because TypeScript's type system is structural, not nominal. Everything except null and undefined is an object, so everything can be assigned to an empty object.

  • If you want to represent an empty object, use Record<string, never> instead.

Video Explanation

Transcript

The Empty Object Type

The empty object type in TypeScript doesn't really behave as you expect. It doesn't represent "any object". Instead, it represents any value that isn't null or undefined.

Try experimenting with it in the playground below:

Here, we are basically typing example1 as an empty object, yet we can pass a string to it. We can pass a number to it. We can pass a boolean to it and any object to it.

The only things we can't pass to it are null or undefined:

ts
const example5: {} = null;
Type 'null' is not assignable to type '{}'.2322Type 'null' is not assignable to type '{}'.
const example6: {} = undefined;
Type 'undefined' is not assignable to type '{}'.2322Type 'undefined' is not assignable to type '{}'.

If you're using ESLint, you might even hit an error:

Don't use {} as a type. {} actually means "any non-nullish value".

This article explains exactly why this is good advice.

The Object Type

This also happens with the Object type, which I think is just an alias over the top of {}:

ts
const obj1: Object = "str";
const obj2: Object = null;
Type 'null' is not assignable to type 'Object'.2322Type 'null' is not assignable to type 'Object'.

So this behaves in exactly the same way. So you shouldn't be using this Object type either.

Representing an Empty Object

If you do want to represent a kind of empty object, then we can use this Record<PropertyKey, never>.

ts
type EmptyObj = Record<PropertyKey, never>;
 
const emptyObj1: EmptyObj = {};
const emptyObj2: EmptyObj = {
foo: "whatever",
Type 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.
};
const emptyObj3: EmptyObj = "str";
Type 'string' is not assignable to type 'EmptyObj'.2322Type 'string' is not assignable to type 'EmptyObj'.
const emptyObj4: EmptyObj = 123;
Type 'number' is not assignable to type 'EmptyObj'.2322Type 'number' is not assignable to type 'EmptyObj'.

What this does is it means that you can pass an empty object. It's going to stop you from passing anything with a property on it. And it's also going to stop you from passing primitives, or null or undefined.

Constraining Functions

The only time this might be useful is if you want a really wide constraint on a function. Let's say you want to make sure that the thing that you're passing that function isn't null or undefined:

ts
const myFunc = (constraint: {}) => {};
 
myFunc("str");
myFunc(123);
myFunc(true);

But even then, you're not going to be able to access any properties on constraint:

ts
const myFunc = (constraint: {}) => {
constraint.foo;
Property 'foo' does not exist on type '{}'.2339Property 'foo' does not exist on type '{}'.
};

You'll hit this error:

Property 'foo' does not exist on type ''.

This is happening because we're trying to access a property on an empty object. So it's not terribly useful.

Generic Functions

The only possible place it could be useful - or at least the one I want to cover here - is as a constraint in a generic function.

ts
const myGenericFunc = <T extends {}>(t: T) => {
return t;
};
 
const result1 = myGenericFunc("str");
const result2 = myGenericFunc(123);
const result3 = myGenericFunc(true);

Inside myGenericFunc, we want to make sure that we can't pass null or undefined into the generic function. If we hover over myGenericFunc here, you can see that it's capturing str and returning it in the results here.

But it fails when we try to pass in null or undefined.

ts
const result4 = myGenericFunc(null);
Argument of type 'null' is not assignable to parameter of type '{}'.2345Argument of type 'null' is not assignable to parameter of type '{}'.
const result5 = myGenericFunc(undefined);
Argument of type 'undefined' is not assignable to parameter of type '{}'.2345Argument of type 'undefined' is not assignable to parameter of type '{}'.

Conclusion

So, you probably shouldn't be using the {} type really anywhere. And you'll have likely come to this article because you found a linting rule that's preventing you from using it. That is why it doesn't represent what you think, but it's occasionally useful for constraining generics.

Got any more questions? Found any more use cases? Let me know:

Matt's signature

Share this article with your friends

`any` Considered Harmful, Except For These Cases

Discover when it's appropriate to use TypeScript's any type despite its risks. Learn about legitimate cases where any is necessary.

Matt Pocock
Matt Pocock

No, TypeScript Types Don't Exist At Runtime

Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.

Matt Pocock
Matt Pocock

Deriving vs Decoupling: When NOT To Be A TypeScript Wizard

In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.

Matt Pocock
Matt Pocock

NoInfer: TypeScript 5.4's New Utility Type

Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.

Matt Pocock
Matt Pocock