Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~27 min· topic 4 of 6

Topic 5.4

object, Object, {} and the Top Types

In one line

TypeScript has several types that mean "almost anything": unknown, {}, object, Object and any. They differ in exactly what they let in and what they let you do, and picking the wrong one is a classic source of bugs.

Think of it like this

Think of a lost-property office with five doors. Door 1 accepts anything at all, and a guard makes you check what each item is before you may use it (unknown). Door 2 accepts anything that is really there, but not "nothing" ({}). Door 3 accepts only proper objects, not small loose items like coins or notes (object). Door 4 also takes anything that is there, but only lets you ask about the few things every item has, such as "what is your label?" (Object). Door 5 has a sign that reads "anything goes, and we won't look inside" (any). The five types in this topic are those five doors. Use door 1 for unchecked things: at door 5 nobody checks, which is why it is dangerous.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Top type
A type that every value fits in. unknown and any are top types.
unknown
Accepts any value, but you must check it before using it. The safe top type.
any
Accepts any value and lets you do anything with it. The checker looks away.
object
Lowercase. Any value that is not a primitive: objects, arrays, functions.
Object
Capital O. The type of what all values inherit, like toString. Avoid it.
{}
Any value except null and undefined. Not "an empty object".
Primitive
A plain value, not an object: string, number, boolean, bigint, symbol, null, undefined.
Narrowing
Using a check like typeof x === "string" so TypeScript knows a more precise type.

Step by step

01Start with the hierarchy

The arrows mean "fits inside". Everything fits into unknown. Take away null and undefined and you have {}. Split {} into things with properties of their own (object) and plain values (the primitives).

Start with the hierarchydiagram
Rendering diagram…

02object: non-primitives only

object accepts objects, arrays and functions, and rejects text, numbers and null. It is useful when you only want to say "this is some kind of thing with properties". But you can't read b.x from it, because object doesn't say which properties exist.

object.tswhole filets
let a: object = { x: 1 };
a = [1, 2];  // ok
a = "hi";    // error TS2322

03{} and Object let primitives in

This surprises most people. {} and Object both accept the number 5 and the string "text". {} rejects only null and undefined. That makes {} a poor choice for "an object". Reach for object or a proper shape instead.

empty.tswhole filets
let e: {} = 5;     // ok
e = "text";        // ok
e = null;          // error TS2322

04unknown: take anything, check before use

Assigning *into* unknown is always allowed. Reading *out of* it is not: you can't call a method or assign it to a number until you narrow it. This makes the check you were going to forget impossible to forget.

unknown.tswhole filets
function show(v: unknown): string {
  if (typeof v === "string") {
    return v.toUpperCase(); // v is string here
  }
  return "?";
}

05Checks narrow unknown to {}

If you only check that a value is not null or undefined, TypeScript knows it is *something* but not what. The result is {}: you may call methods that every value has (like toString()), and nothing else.

narrow.tswhole filets
function check(v: unknown) {
  if (v !== null && v !== undefined) {
    const w = v; // w: {}
    return w.toString();
  }
}

Try it yourself

  1. 1

    Narrow it properly

    In the second example, replace const n: number = u; with if (typeof u === "number") { const n: number = u; }. Predict: does the error go away? Yes, because inside the if, u is a number.

  2. 2

    Swap in any

    Change let u: unknown = null; to let u: any = null;. Predict what happens to the error on const n: number = u;: it disappears. Now add u.whatever.deep(); and notice the checker stays silent; the program would crash at runtime.

  3. 3

    Which door?

    For each value, predict object, {} and unknown as yes or no, then check with three assignments: [], "x", undefined. Expected: [] fits all three; "x" fits {} and unknown but not object; undefined fits only unknown.

Code & diagrams

object accepts non-primitives only

Arrays and functions are objects, so they are fine. Strings, numbers and null are rejected. Even though b really holds { x: 1 }, reading b.x is an error: the type object doesn't say which properties exist. The code still runs.

Console

(nothing was printed)

Expected console output

null
1
{}, Object and unknown side by side

{} and Object take primitives. Only {} rejects null and undefined. unknown takes everything, but you can't put an unknown into a number variable until you check it. Notice e prints undefined at the end: the type errors didn't stop the code.

Console

(nothing was printed)

Expected console output

HI ? undefined 5
What a null check leaves behind

After the check, v is {}: something that exists. toString() works because every value has it. But plain is really an array, and plain.length is an error because {} says nothing about length. Object has hasOwnProperty and toString, which is all it knows.

Console

(nothing was printed)

Expected console output

5 nothing
true [object Object]

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read a property from object

The first example already contains this mistake: const b: object = { x: 1 }; followed by b.x.

terminal
$ Types panel
── what you'll see ──
index.ts(9,15): error TS2339: Property 'x' does not exist on type 'object'.

Break #2

Use an unchecked unknown

The second example already contains this mistake: u is an unknown and the line const n: number = u; uses it without a check.

terminal
$ Types panel
── what you'll see ──
index.ts(10,7): error TS2322: Type 'unknown' is not assignable to type 'number'.

Myth vs fact

Myth

{} means an empty object.

Fact

As a type it means "any value except null and undefined". Numbers and strings fit. To mean "an object with no properties", use Record<string, never>; to mean any object, use object.

Myth

Object and object are the same.

Fact

object is any non-primitive. Object is the shape of what every value inherits, so primitives fit it. Always use lowercase object.

Myth

unknown is just a nicer-looking any.

Fact

any turns checking off. unknown keeps it on and forces a check before use. They are opposites in safety, though both accept every value.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    In the type lattice, unknown is the top, never is the bottom, and {} is equal to NonNullable<unknown>. Since TypeScript 4.8, unknown & {} simplifies to {}, and narrowing unknown by !== null && !== undefined produces {}, as the live example shows. That also changed NonNullable<T> to be T & {}.

  • ▸

    With strictNullChecks off, null and undefined are in every type, so {} accepts them and the whole distinction in this topic partly collapses. This is one more reason to keep strict on (it is on by default in TypeScript 6 and 7).

  • ▸

    The catch (e) variable is unknown under useUnknownInCatchVariables (part of strict, introduced in 4.4), which is why (e as Error).message shows up so often in this course. Narrowing with e instanceof Error is the safer form.

  • ▸

    Generic constraints use these types. T extends {} excludes null and undefined (a common idiom after 4.8), T extends object excludes primitives, and T extends unknown is the same as no constraint. The difference changes whether T can be inferred from 5 or from null. You'll use this in Phase 7.

Remember this

  1. 1

    The problem this solves: Some values can be anything, and the types any, unknown, {} and object look alike but accept different things. Picking the wrong one either blocks valid code or lets bugs through. **unknown is the safe top type (top type** means every value fits in it). It accepts every value, including null and undefined. In return it lets you do nothing with the value until you check what it is: no property access, no calls, and you can't assign it to anything more specific. You narrow it with typeof, instanceof, in and so on (Topic 4.3, Topic 4.4). unknown was added in TypeScript 3.0 as the safe replacement for any and is the right type for data you haven't checked yet: JSON.parse results, catch variables, API responses.

  2. 2

    **any is the unsafe top type. It accepts everything and** lets you do everything: access any property, call it, assign it to any other type. It switches the checker off for that value, and the "off" spreads, because anything calculated from an any is also any. Prefer unknown every time; Topic 1.6 covers any and why it is a last resort.

  3. 3

    **{}** (an empty object type) means "any value that is not null or undefined". That includes numbers, strings, booleans, arrays and functions, because TypeScript thinks of a primitive as having the properties of its wrapper (a number has toFixed). {} is not "an empty object": let e: {} = 5 is legal. And you can't read any property from it: it only promises "something exists". After you test v !== null && v !== undefined on an unknown, TypeScript narrows it to {}, as the live example shows.

  4. 4

    **object** (lowercase) means "any non-primitive value": objects, arrays, functions, class instances, Date, Map. It rejects the primitives string, number, boolean, bigint, symbol, null and undefined. It is the right type for "any thing that has properties", for example a parameter for Object.keys-style helpers. You can't read specific properties from it, since TypeScript doesn't know which ones exist.

  5. 5

    **Object** (capital O) is the type of the properties every value inherits: toString, valueOf, hasOwnProperty and a few more. Because primitives have those too, let o: Object = 5 is allowed. It looks like object but behaves like {} with a few methods added, which is why **style guides and typescript-eslint forbid Object** (its no-wrapper-object-types rule; no-unsafe-function-type does the same for the type Function, and no-empty-object-type flags {}; the old ban-types rule was replaced by these in typescript-eslint 8). Use object, unknown or a precise shape instead.

  6. 6

    The picture to hold: unknown is the top; one level below it is {} (unknown minus null and undefined); below {} are object (non-primitives) and the primitives. never (Topic 5.5) is at the bottom, with nothing in it. As an everyday rule: a value you haven't checked is unknown; a value that must be "any object" is object; a value that must be a specific shape gets a specific type like { id: number } or Record<string, unknown> (Topic 3.6).

  7. 7

    Why does {} accept a string if it "is not an object"? Because TypeScript is structural (Topic 3.5): it asks "does this value have at least the properties required?" The type {} requires no properties, and a string has all the ones it requires (none), so it fits. The same logic is why object, which means "not a primitive", is a separate, special type: structure alone can't express it.

Explain it without notes

01

What is the difference between unknown and any?

02

What do object, Object and {} accept, and which should you use?

03

What is v inside if (v !== null && v !== undefined) when v: unknown, and what can you do with it?

04

Why is JSON.parse result best treated as unknown, even though TypeScript types it as any?

Practice

01

Write a function describe(v: unknown): string that returns "text" for strings, "number" for numbers, "list" for arrays and "other" for everything else. Which narrowing check recognises arrays?

02

Write function keysOf(o: object): string[] that returns Object.keys(o). Which of these calls compile: keysOf({ a: 1 }), keysOf([1]), keysOf("hi"), keysOf(null)?

03

Write a function that takes unknown, and returns its length if it is a string or an array, otherwise 0.

Trade-offs

  • ↔

    unknown forces a check at every use, which costs a few lines but makes bad data visible at the edge of your program instead of deep inside it. any saves those lines and buys bugs.

  • ↔

    object is looser than a real shape and gives you less to work with; a precise interface is better whenever you know the properties. Keep the wide types for the genuinely unknown places.

Done when you can

  • I can say what unknown, any, {}, object and Object each accept.

  • I can explain why {} is not "an empty object".

  • I can narrow an unknown with typeof, Array.isArray and a null check.

  • I can say what v becomes after v !== null && v !== undefined on an unknown.

  • I can choose unknown over any and object over Object, and say why.