What's the difference between object
and {}
in TypeScript?
— by Gabriel Vergnaud · Sep 10, 2023
I used to think that object
was an alias for the {}
"empty object" type in TypeScript, but I was wrong! These two types represent different sets of values, and if you want to write accurate type definitions for your functions and classes, it's important to know which one to choose.
Let's see how they differ!
The {}
empty object type
The empty object {}
type is one of the largest sets of values. All types with properties or methods are assignable to it, including primitive types, like number
, string
and boolean
!
This implies that a function taking {}
will happily accept most values:
function fn(something: {}) {
/* 👆
The empty object type! */
}
// ✅ these type-check:
fn({ token: "..." });
fn({});
fn("Hi!");
fn(1337);
fn(() => "a function!");
function fn(something: {}) {
/* 👆
The empty object type! */
}
// ✅ these type-check:
fn({ token: "..." });
fn({});
fn("Hi!");
fn(1337);
fn(() => "a function!");
Only two values aren't assignable to the empty object: null
and undefined
.
fn(null);
fn(undefined);
fn(null);
fn(undefined);
This is the only difference between {}
and unknown
by the way. unknown
contains every single JavaScript value, including null
and undefined
:
The object
type
Now let's look at the object
type. Unlike {}
, object
does not include primitive types!
Here is what happens if you try assigning a primitive type like a string
or a number
to object
:
let a: object;
a = 403; // ❌
a = "⛔️"; // ❌
a = { some: "object" }; // ✅
let a: object;
a = 403; // ❌
a = "⛔️"; // ❌
a = { some: "object" }; // ✅
It doesn't type-check!
But what about functions and arrays?
let b: object;
// ✅
b = [1, 2, 3];
b = (n: number) => n.toString();
let b: object;
// ✅
b = [1, 2, 3];
b = (n: number) => n.toString();
They are assignable to the object
type too. Only primitive types aren't.
Which one should you use?
When writing functions taking objects, you usually want to use the object
type. For example, consider the following merge
function:
const merge = <
A extends {},
B extends {}
>(a: A, b: B): A & B => ({
...a,
...b,
});
It won't throw if you give it a string or a number, but the result will look pretty weird:
const result = merge(42, "what?"); // type-checks
// => {0: 'w', 1: 'h', 2: 'a', 3: 't', 4: '?'}
Is this really what you expected? Probably not.
If you constrain your type parameters with object
instead of {}
, passing numbers or strings will simply not type-check:
const merge = <A extends object, B extends object>(a: A, b: B): A & B => ({
...a,
...b,
});
const result = merge(42, "what?"); // ❌
const merge = <A extends object, B extends object>(a: A, b: B): A & B => ({
...a,
...b,
});
const result = merge(42, "what?"); // ❌
No one will ever mistakenly pass a string to your merge
function again! 🎉
Summary 📚
Here are the main takeaways from this article:
- Types are sets of values.
{}
includes all primitive types.object
does not.
The behavior of the {}
type isn't super intuitive, so my advice is to use the object
type unless you really know what you are doing!
If you liked this article, chances are you'll like Type-Level Typescript. It's an advanced TypeScript course that will give you a solid understanding of the type system's fundamentals and guide you through its most advanced features. Enroll Now!
Subscribe to the newsletter!
Receive all new chapters and articles from Type-Level TypeScript directly in your inbox!