Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~21 min· topic 5 of 7

Topic 1.5

Object Types

In one line

An object type lists the properties a value must have and the type of each: { name: string; age: number }. Properties can be optional (?) or read-only, and a fresh object literal may not carry unlisted extras.

Think of it like this

A form with labelled boxes: Name, Age, Email (optional). A clerk can tell at a glance if a form is missing the name, if someone wrote letters in the Age box, or if there is a stray box that doesn't belong. An object type is that form. It lists the boxes, says what goes in each, and says which can stay empty.

Words you'll meet

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

Object
A bundle of named values, written { name: "Asha", age: 30 }.
Property
One named value in an object. name is a property of the object above.
Object type
A description of an object's shape: { name: string; age: number }.
Optional property
A property marked with ? that is allowed to be missing.
readonly property
A property that code can read but not assign to.
Object literal
An object written out directly in the code with { ... }.
Fresh literal
An object literal that has just been written in place and not yet stored in a variable. Only these get the excess property check.
Excess property
A property in an object literal that the target type doesn't list.
exactOptionalPropertyTypes
A flag that treats "missing" and "present but undefined" as different things.

Step by step

01Write the shape

Put the property names and their types between braces. Both ; and , work as separators.

The same shape works in a parameter, which is where object types appear most often: the function says exactly what it needs.

shape.tswhole filets
const user: { name: string; age: number } = {
  name: "Asha",
  age: 30,
};

function show(p: { x: number; y: number }) {
  return p.x + "," + p.y;
}

02Optional with ?

Add ? after the name. Now email may be left out, and reading it gives string | undefined.

That | undefined is TypeScript reminding you the value might not be there. The next two topics show how to handle that.

optional.tswhole filets
const user: { name: string; age: number; email?: string } = {
  name: "Asha",
  age: 30,
};
const mail = user.email;   // string | undefined

03readonly: set once, read forever

Mark a property readonly and assignments to it are refused. Other properties stay changeable.

The compiler is the only guard. At runtime the property can still change, so don't use readonly for security.

04Excess properties: typos in object literals

A fresh object literal is checked for extra properties. This catches colour written where color was meant.

A variable that holds an object with extras is accepted: the value still has every property the type asks for.

Excess properties: typos in object literalsdiagram
Rendering diagram…

05Missing is not the same as undefined

With exactOptionalPropertyTypes, nickname?: string accepts a missing property or a string. Writing nickname: undefined is an error.

This matches how JavaScript behaves: "nickname" in obj and Object.keys can tell the difference. The flag is strict, and some libraries need nickname?: string | undefined.

Try it yourself

  1. 1

    Remove a required property

    In the first example delete age: 30,. Predict the error before you look: the message names the missing property (TS2741: Property 'age' is missing in type '{ name: string; }' but required in type ...).

  2. 2

    Typo the key

    In the second example change y: 2 in line 2 to yy: 2. There are two problems (a missing y and an unknown yy) but the checker reports one: TS2353 on yy. Fix it, and then the missing y appears. Errors often come one layer at a time.

  3. 3

    Allow undefined on purpose

    In the third example change line 1 to nickname?: string | undefined. The error on line 3 disappears. Which behaviour do you want in your own code?

Code & diagrams

Optional and readonly properties

Changing label is allowed, assigning id is not. But look at the console: the id really became 2, because readonly vanishes when the code is turned into JavaScript.

Console

(nothing was printed)

Expected console output

Asha undefined
{ id: 2, label: 'B' }
Excess property check: fresh literals only

The first line is rejected, but the second route (through the variable q) is accepted. The type alias on line 1 just names the shape (Topic 3.1).

Console

(nothing was printed)

Expected console output

{ x: 1, y: 2, z: 3 } { x: 1, y: 2, z: 3 }
exactOptionalPropertyTypes: missing is not undefined

This flag is on in every example here because tsc --init turns it on. In a project without it, line 3 would be fine. Change line 1 to nickname?: string | undefined and the error goes away.

Console

(nothing was printed)

Expected console output

{ name: 'A' } { name: 'C', nickname: 'Cee' }

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

Add an unknown property to a fresh literal

In the second example, line 2 adds z: 3 to an object that must be a Point.

terminal
$ Types panel
── what you'll see ──
index.ts(2,32): error TS2353: Object literal may only specify known properties, and 'z' does not exist in type 'Point'.

Break #2

Assign undefined to an exact optional property

In the third example, line 3 sets nickname: undefined.

terminal
$ Types panel
── what you'll see ──
index.ts(3,7): error TS2375: Type '{ name: string; nickname: undefined; }' is not assignable to type 'Profile' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Types of property 'nickname' are incompatible.
Type 'undefined' is not assignable to type 'string'.

Myth vs fact

Myth

An optional property email?: string is the same as email: string | undefined.

Fact

With email: string | undefined the key must be written, even if its value is undefined. With email?: string the key may be left out entirely. They differ in how objects are created and in checks like "email" in obj.

Myth

readonly stops the property from ever changing.

Fact

It only stops the compiler from accepting an assignment through that name. In the JavaScript that runs, nothing is frozen.

Myth

Extra properties are always an error.

Fact

Only in a fresh object literal. Through a variable they are accepted, because TypeScript compares shapes (Topic 3.5).

Pro corner

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

  • ▸

    Excess property checking is a heuristic layered on top of structural assignability: freshness is a flag on the object literal's type that disappears once the literal is stored in a variable. Topic 16.1 shows the internals, including why spreading doesn't always trigger it.

  • ▸

    exactOptionalPropertyTypes applies to every optional property, including the ones Partial<T> creates (Topic 10.1), so an update object typed Partial<User> also refuses an explicit undefined. It breaks code that writes obj.prop = undefined to "unset" a property; the fix is to delete obj.prop or widen the property type. It is not part of strict; tsc --init enables it.

  • ▸

    Object types are compared structurally: only the properties present in the target matter. That is how two unrelated classes with the same fields are interchangeable, and the reason readonly doesn't affect assignability (a { readonly x: number } is assignable to { x: number }, a known soundness hole).

Remember this

  1. 1

    The problem this solves: Objects are passed around with a missing field or a misspelt property name, and the code reads undefined instead of failing early. An object type lists exactly which properties must exist. An object groups related values under names called properties: { name: "Asha", age: 30 } has two properties, name and age. Its type, an object type, is written the same way with types in place of values: { name: string; age: number }. Separate the properties with ; (or ,), and write the whole thing wherever a type fits: on a variable, a parameter or a return value.

  2. 2

    The checker holds you to the list in three ways. Every listed property must be present, each must have the right type, and when you read user.age the checker knows it is a number. Reading a property that isn't listed (user.nmae) is TS2339, which is how typos in property names get caught (Topic 0.1).

  3. 3

    A question mark after a property name makes it optional: email?: string means the property may be missing. Reading it gives string | undefined, so you must deal with the missing case before using it (Topic 1.7). Optional is about *the property being absent*, which is not quite the same as a property that is present and set to undefined. A flag called exactOptionalPropertyTypes (on in the tsc --init file) keeps the two apart: with it, email?: string accepts a missing email or a string, but not email: undefined. If you want to allow it, write email?: string | undefined.

  4. 4

    **readonly** before a property name means code may read it but not assign to it: readonly id: number. Assigning is TS2540. It is the right tool for things like ids, which should be set once. Like every type feature it is checked only by the compiler and removed from the JavaScript, so it is a promise to your teammates and to yourself, not a lock (the example below shows the value really changing at runtime). It is also shallow: it protects the property, not the object inside it.

  5. 5

    Excess property checks. When you write an object literal directly where a type is expected, any property not in the type is an error: { x: 1, y: 2, z: 3 } assigned to { x: number; y: number } fails with TS2353, because a stray z is probably a typo (colour instead of color). The check applies only to fresh object literals (an object written right there). Put the same object in a variable first and then pass the variable, and there is no error, because TypeScript compares shapes and an object with extra properties still has the properties you asked for (Topic 3.5).

  6. 6

    Object types nest: a property can itself be an object type (address: { city: string }) or an array. Writing a long type inline several times is tiring and error-prone, so soon you will give it a name with a type alias or an interface (Phase 3). For now think of the inline form as the raw material those are made of.

Explain it without notes

01

How do you describe an object's shape in TypeScript, and what does the checker verify?

02

What is the difference between email?: string and email: string | undefined?

03

What is an excess property check and when does it not apply?

04

What does readonly on a property guarantee?

Practice

01

Write a function describe(user) where user has a required name (text), a required age (number) and an optional email (text). It returns name followed by the email, or no email when it is missing. It must compile.

02

Why does the second line below compile while the first doesn't? Fix the first line without changing the type. ``ts const a: { id: number } = { id: 1, extra: true }; const obj = { id: 1, extra: true }; const b: { id: number } = obj;

Trade-offs

  • ↔

    Inline object types are quick for one-off parameters but repeat badly. Name a shape (Phase 3) when it appears twice or more.

  • ↔

    exactOptionalPropertyTypes is more accurate but can make Partial-style update code and some library types awkward. Teams that dislike the friction turn it off; the cost is that "missing" and "undefined" merge.

Done when you can

  • I can write an object type with required, optional and readonly properties.

  • I can predict what reading an optional property gives.

  • I can explain when the excess property check fires and when it does not.

  • I can explain what exactOptionalPropertyTypes changes.

  • I can say what readonly does not do at runtime.