skip to content

In TypeScript, which values are assignable to the type `{}`, how does that differ from the type `object`, and why is `{}` a poor way to say "some object"?

level: middleimportance: should knowfreq 40%

answer

  1. a target with no members rejects nothing
  2. primitives are compared through apparent members
  3. only null and undefined fail
  4. object means non-primitive, {} does not

basics

~20 s

{} requires no members, so under strictNullChecks every value except null and undefined is assignable to it, primitives included. object excludes primitives but still accepts any non-primitive. Neither lets you read a property, so {} says almost nothing.

solid answer

~50 s

`{}` declares an object type with zero required members, and assignability only checks the members the target declares — so there is nothing to fail. Under `strictNullChecks` that makes `{}` mean "anything except `null` and `undefined`": numbers, strings, booleans and functions are all assignable, because primitives are compared against their apparent members from `Number`, `String` and so on. The type `object` is different — it means "non-primitive", so `42` is rejected while `{ a: 1 }` and `() => {}` are accepted. Both are near-useless for reading: a value typed `{}` has no declared members, so any property access is an error and you must narrow first. If you mean "an object with unknown string-keyed contents", write `Record<string, unknown>`; if you mean "a value I know nothing about", write `unknown`, which unlike `{}` also admits `null` and `undefined`.

code

typescript · 9 lines
typescript
const a: {} = 42;
const b: {} = "text";
const c: {} = { any: 1 };

const d: object = { any: 1 };
const e: Record<string, unknown> = { any: 1 };
const f: unknown = null;

console.log(a, b, c, d, e, f);

go deeper

for a junior

Know that {} is not "an empty object" — it accepts numbers and strings too — and that when you mean "a value I know nothing about" the right annotation is unknown.

for a middle

Derive the behaviour from the rule: assignability checks only the target's members, and {} has none, so everything but null and undefined passes. Contrast that with object, which excludes primitives.

for a senior

Explain why a {} parameter is a latent defect in an API — it advertises an intent it does not enforce — and prescribe the alternatives by intent: unknown at boundaries, Record<string, unknown> for bags, a real interface otherwise.

for a principal

Take a position on top-type policy across a codebase: where untyped data enters, unknown plus a validator is the enforceable rule, and permissive placeholders like {} or Object should be lint-blocked rather than left to reviewer memory.

## Why `{}` accepts a number Assignability walks the members the *target* declares. `{}` declares none. So for almost any source, the walk finishes immediately with nothing to check, and the assignment succeeds: ```ts const a: {} = 42; // ok const b: {} = "text"; // ok const c: {} = () => 1; // ok const d: {} = { any: 1 }; // ok // const e: {} = null; // error under strictNullChecks // const f: {} = undefined; // error under strictNullChecks ``` Primitives pass because TypeScript compares them through their **apparent type** — the members a `number` gets from the `Number` interface, a `string` from `String`, and so on. A primitive is not a bare scalar to the checker; it has methods, and it certainly has the zero members `{}` asks for. The only values excluded are `null` and `undefined`, which have no members at all, and only when `strictNullChecks` is on. Turn that flag off and even they are assignable, since `null` and `undefined` are then assignable to everything. So the accurate reading of `{}` is **"any non-nullish value"**, not "an empty object". ## `{}` versus `object` versus `Object` - **`{}`** — any non-nullish value, primitives included. - **`object`** (lowercase, a real keyword type) — any **non-primitive**: object literals, arrays, functions, class instances. `const n: object = 42` is an error. - **`Object`** (capital, the interface from the standard library) — declares `toString`, `valueOf`, `hasOwnProperty` and friends, which primitives also have through their apparent types, so it behaves much like `{}` in practice. Using it is generally discouraged precisely because it reads like `object` and does not behave like it. - **`unknown`** — the true top type: everything is assignable to it, `null` and `undefined` included, and nothing may be done with it before narrowing. ```ts const g: object = { a: 1 }; // ok // const h: object = 42; // error — primitives excluded const i: unknown = null; // ok — unknown admits everything ``` ## Why it is a poor "some object" Two reasons. First, it does not mean what the reader thinks. Someone writing `function f(x: {})` almost always intends "pass me an object"; the signature actually accepts `f(1)` and `f("no")`, and the mistake surfaces far from the declaration. Second, it gives you nothing to work with. There are no declared members, so `x.id` is an error, and iterating requires narrowing first — `typeof x === "object" && x !== null`, then an `in` check or a validator. If you have to narrow anyway, `unknown` is the more honest starting point: it forces the narrowing instead of quietly letting a `number` slide through. The alternatives, chosen by intent: ```ts function takesAnything(x: unknown) {} // I know nothing yet function takesSomeObject(x: object) {} // non-primitive, opaque function takesBag(x: Record<string, unknown>) {} // string-keyed, contents unknown ``` ## Where `{}` is genuinely the right tool It has one real, deliberate use: as the "non-nullish" constraint. Intersecting a type with `{}` removes `null` and `undefined` from it, which is exactly how the standard library's `NonNullable<T>` is defined in TypeScript 5.x — `T & {}`. The same trick appears in generic constraints, `T extends {}`, to say "any non-nullish type" without excluding primitives. ```ts type A = NonNullable<string | null>; // string type B = string | undefined & {}; // careful: & binds tighter than | ``` That second line is a good reminder to parenthesise: `(string | undefined) & {}` is what removes the `undefined`. ## How to answer Lead with the mechanism — a target with no required members has nothing to reject — then give the boundary (`null` and `undefined`, under `strictNullChecks`), then contrast with `object` and `unknown`, and finish with the one legitimate use as a non-nullish constraint. That order shows you derived the behaviour from the assignability rule rather than memorising a curiosity.

  • Is `unknown` assignable to `{}`?
    No. `unknown` includes `null` and `undefined`, which `{}` excludes under `strictNullChecks`, so the assignment is rejected until you narrow. That asymmetry is the clearest demonstration that `{}` is not the top type — `unknown` is, and `{}` sits just below it as "everything except the two nullish values".
  • If `{}` rejects nothing, why does reading a property off a `{}`-typed value fail?
    Assignability and member access are different operations. The type declares no members, so there is nothing for a property access to resolve against, and the compiler errors. Being permissive about what may be *assigned in* says nothing about what may be *read out* — that is decided entirely by the declared members.
  • When would you deliberately write `T & {}`?
    To strip `null` and `undefined` from `T` while keeping everything else, including primitives. That is exactly how `NonNullable<T>` is defined in TypeScript 5.x. It also appears in generic constraints as `T extends {}`, meaning "any non-nullish type" — broader than `T extends object`, which would exclude strings and numbers.

saying these in an interview costs you the question

  • Says `{}` means an object with no properties
  • Thinks `{}` and `object` accept the same values
  • Claims `{}` is the top type that accepts everything
  • Uses `{}` as a parameter type meaning "any object"
  • Believes you can read arbitrary properties off a `{}` value

context