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.
unknownandanyare 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
nullandundefined. 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).
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.
let a: object = { x: 1 };
a = [1, 2]; // ok
a = "hi"; // error TS232203{} 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.
let e: {} = 5; // ok
e = "text"; // ok
e = null; // error TS232204unknown: 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.
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.
function check(v: unknown) {
if (v !== null && v !== undefined) {
const w = v; // w: {}
return w.toString();
}
}Try it yourself
- 1
Narrow it properly
In the second example, replace
const n: number = u;withif (typeof u === "number") { const n: number = u; }. Predict: does the error go away? Yes, because inside theif,uis anumber. - 2
Swap in any
Change
let u: unknown = null;tolet u: any = null;. Predict what happens to the error onconst n: number = u;: it disappears. Now addu.whatever.deep();and notice the checker stays silent; the program would crash at runtime. - 3
Which door?
For each value, predict
object,{}andunknownas yes or no, then check with three assignments:[],"x",undefined. Expected:[]fits all three;"x"fits{}andunknownbut notobject;undefinedfits onlyunknown.
Code & diagrams
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{} 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 5After 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.
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.
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,
unknownis the top,neveris the bottom, and{}is equal toNonNullable<unknown>. Since TypeScript 4.8,unknown & {}simplifies to{}, and narrowingunknownby!== null && !== undefinedproduces{}, as the live example shows. That also changedNonNullable<T>to beT & {}. - ▸
With
strictNullChecksoff,nullandundefinedare in every type, so{}accepts them and the whole distinction in this topic partly collapses. This is one more reason to keepstricton (it is on by default in TypeScript 6 and 7). - ▸
The
catch (e)variable isunknownunderuseUnknownInCatchVariables(part ofstrict, introduced in 4.4), which is why(e as Error).messageshows up so often in this course. Narrowing withe instanceof Erroris the safer form. - ▸
Generic constraints use these types.
T extends {}excludesnullandundefined(a common idiom after 4.8),T extends objectexcludes primitives, andT extends unknownis the same as no constraint. The difference changes whetherTcan be inferred from5or fromnull. You'll use this in Phase 7.
Remember this
- 1
The problem this solves: Some values can be anything, and the types
any,unknown,{}andobjectlook alike but accept different things. Picking the wrong one either blocks valid code or lets bugs through. **unknownis the safe top type (top type** means every value fits in it). It accepts every value, includingnullandundefined. 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 withtypeof,instanceof,inand so on (Topic 4.3, Topic 4.4).unknownwas added in TypeScript 3.0 as the safe replacement foranyand is the right type for data you haven't checked yet:JSON.parseresults,catchvariables, API responses. - 2
**
anyis 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 ananyis alsoany. Preferunknownevery time; Topic 1.6 coversanyand why it is a last resort. - 3
**
{}** (an empty object type) means "any value that is notnullorundefined". That includes numbers, strings, booleans, arrays and functions, because TypeScript thinks of a primitive as having the properties of its wrapper (a number hastoFixed).{}is not "an empty object":let e: {} = 5is legal. And you can't read any property from it: it only promises "something exists". After you testv !== null && v !== undefinedon anunknown, TypeScript narrows it to{}, as the live example shows. - 4
**
object** (lowercase) means "any non-primitive value": objects, arrays, functions, class instances,Date,Map. It rejects the primitivesstring,number,boolean,bigint,symbol,nullandundefined. It is the right type for "any thing that has properties", for example a parameter forObject.keys-style helpers. You can't read specific properties from it, since TypeScript doesn't know which ones exist. - 5
**
Object** (capital O) is the type of the properties every value inherits:toString,valueOf,hasOwnPropertyand a few more. Because primitives have those too,let o: Object = 5is allowed. It looks likeobjectbut behaves like{}with a few methods added, which is why **style guides and typescript-eslint forbidObject** (itsno-wrapper-object-typesrule;no-unsafe-function-typedoes the same for the typeFunction, andno-empty-object-typeflags{}; the oldban-typesrule was replaced by these in typescript-eslint 8). Useobject,unknownor a precise shape instead. - 6
The picture to hold:
unknownis the top; one level below it is{}(unknown minusnullandundefined); below{}areobject(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 isunknown; a value that must be "any object" isobject; a value that must be a specific shape gets a specific type like{ id: number }orRecord<string, unknown>(Topic 3.6). - 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 whyobject, which means "not a primitive", is a separate, special type: structure alone can't express it.
Explain it without notes
What is the difference between unknown and any?
What do object, Object and {} accept, and which should you use?
What is v inside if (v !== null && v !== undefined) when v: unknown, and what can you do with it?
Why is JSON.parse result best treated as unknown, even though TypeScript types it as any?
Practice
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?
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)?
Write a function that takes unknown, and returns its length if it is a string or an array, otherwise 0.
Trade-offs
- ↔
unknownforces 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.anysaves those lines and buys bugs. - ↔
objectis 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,{},objectandObjecteach accept.I can explain why
{}is not "an empty object".I can narrow an
unknownwithtypeof,Array.isArrayand a null check.I can say what
vbecomes afterv !== null && v !== undefinedon anunknown.I can choose
unknownoveranyandobjectoverObject, and say why.