Skip to content
aviral gupta

// B2.3 · ~28 min · Beginner

Object types and optional properties

After this lesson you can describe objects with object types, check optional properties for undefined before use, and fix the typos excess property checks find.

Lesson 3 of 5 in B2 Everyday types

You will be able to

  • Annotate a variable or parameter with an object type, and read the errors for a missing, extra or wrongly typed property
  • Mark a property optional with ?, and check it for undefined before using it
  • Explain when excess property checks apply, and fix the typo they report
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript objects: which of these are true in plain JavaScript? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: which line does tsc report?

    const book: { title: string; year: number } = { title: "Dune", year: 1965 };
    console.log(book.title.toUpperCase());
    console.log(book.author);
  3. Practice · Activity 3 of 7

    Fill in the gap so that last may be left out: tsc should accept both calls, and the program prints Bob, then Alice Alisson.

    function printName(obj: { first: string; last ____: string }) {
      return obj.last === undefined ? obj.first : obj.first + " " + obj.last;
    }
    
    console.log(printName({ first: "Bob" }));
    console.log(printName({ first: "Alice", last: "Alisson" }));
    function printName(obj: { first: string; last : string }) {
  4. Practice · Activity 4 of 7

    last is optional. What does tsc report?

    function printName(obj: { first: string; last?: string }) {
      console.log(obj.first + " " + obj.last.toUpperCase());
    }
  5. Practice · Activity 5 of 7

    box has a property the parameter type does not mention. What does tsc report?

    function area(box: { width: number; height: number }) {
      return box.width * box.height;
    }
    
    const box = { width: 2, height: 3, colour: "red" };
    console.log(area(box));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. tsc reports no errors. What does node main.ts print?

    function count(item: { quantity?: number }) {
      return item.quantity || 1;
    }
    
    console.log(count({ quantity: 0 }));
  7. Apply · Activity 7 of 7

    Mini-task. Write a function formatAddress that takes one object with a street, a city and an optional postcode, all strings, and returns Hauptstr. 1, Bonn without a postcode or Hauptstr. 1, 53111 Bonn with one. Annotate the parameter with an object type and check the postcode for undefined. Call it both ways. Then misspell a property in one call, such as sity, and read what tsc says.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Order lines with optional properties

formatLine takes one object. name and price are required; quantity and note are optional, so callers leave them out when they have nothing to say. The function gives quantity a default with ?? and checks note for undefined before it calls toUpperCase. The last two lines annotate a variable with an object type. Run npx tsc, then node main.ts. Then delete the if around the note line and run tsc again, or add a property the type does not know to one of the calls.

main.ts

// A shop's order lines. quantity and note are optional: callers may leave them out.
function formatLine(item: { name: string; price: number; quantity?: number; note?: string }): string {
  const quantity = item.quantity ?? 1;
  let line = item.name + " x" + quantity + " = " + (item.price * quantity).toFixed(2);
  if (item.note !== undefined) {
    line += " (" + item.note.toUpperCase() + ")";
  }
  return line;
}

console.log(formatLine({ name: "Coffee", price: 3.2 }));
console.log(formatLine({ name: "Bagel", price: 2.5, quantity: 2 }));
console.log(formatLine({ name: "Tea", price: 2.8, quantity: 3, note: "no sugar" }));

const order: { id: number; customer: string } = { id: 17, customer: "Ada" };
console.log("Order", order.id, "for", order.customer);

Run it with

npx tsc
node main.ts

Output

Coffee x1 = 3.20
Bagel x2 = 5.00
Tea x3 = 8.40 (NO SUGAR)
Order 17 for Ada
  • The first call passes neither quantity nor note, and tsc accepts it: both are optional.
  • item.quantity ?? 1 gives 1 only when quantity is undefined; a quantity of 0 would stay 0.
  • Inside the if, TypeScript knows note is a string, so toUpperCase is allowed.
  • order is annotated with an object type; reading order.total would be error TS2339.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Give the parameter an object type

tsc rejects this function: Parameter 'book' implicitly has an 'any' type. Annotate book with an object type that lists the three properties the function reads: title and author are strings, year is a number. Add the return type string too. Run the tests: the first one is the type-check.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    An object type goes after the parameter name, like any annotation: book: { … }.

  2. Hint 2

    Inside the braces, list each property with its type, separated by ;: title: string; author: string; year: number.

  3. Hint 3

    The finished first line is export function describeBook(book: { title: string; author: string; year: number }): string {

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function describeBook(book: { title: string; author: string; year: number }): string {
  return book.title + " by " + book.author + " (" + book.year + ")";
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function describeBook(book) {
  return book.title + " by " + book.author + " (" + book.year + ")";
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {describeBook} from './main.ts';

test('describeBook gives title, author and year', () => {
  assert.equal(describeBook({title: 'Dune', author: 'Frank Herbert', year: 1965}), 'Dune by Frank Herbert (1965)', 'describeBook should give Dune by Frank Herbert (1965)');
});

test('describeBook works for another book', () => {
  assert.equal(describeBook({title: 'Emma', author: 'Jane Austen', year: 1815}), 'Emma by Jane Austen (1815)', 'describeBook should give Emma by Jane Austen (1815)');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

Check the optional discount

priceLabel should return the name alone when there is no discount, and the name with the discount otherwise: Mug (-15%). A discount of 0 was given on purpose and is still shown: Pen (-0%). tsc reports 'product.discount' is possibly 'undefined', and a product without a discount crashes. Check discount for undefined before you use it.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    discount has the type number | undefined. Handle the undefined case first and return early.

  2. Hint 2

    if (!product.discount) also catches 0, which the third test passes on purpose. Compare with undefined instead.

  3. Hint 3

    Add if (product.discount === undefined) return product.name; before the existing return.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function priceLabel(product: { name: string; discount?: number }): string {
  if (product.discount === undefined) return product.name;
  return product.name + " (-" + product.discount.toFixed(0) + "%)";
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function priceLabel(product: { name: string; discount?: number }): string {
  return product.name + " (-" + product.discount.toFixed(0) + "%)";
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {priceLabel} from './main.ts';

test('a product without a discount shows its name only', () => {
  assert.equal(priceLabel({name: 'Book'}), 'Book', 'priceLabel({name: "Book"}) should be Book');
});

test('a discount of 15 is shown', () => {
  assert.equal(priceLabel({name: 'Mug', discount: 15}), 'Mug (-15%)', 'priceLabel should give Mug (-15%)');
});

test('a discount of 0 is shown too', () => {
  assert.equal(priceLabel({name: 'Pen', discount: 0}), 'Pen (-0%)', 'a discount of 0 was passed, so it should give Pen (-0%)');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

A required property left out

const user: { name: string; email: string } = { name: "Ada" };
console.log(user.name);

What tsc or Node.js prints

main.ts(1,7): error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type '{ name: string; email: string; }'.

Why, and the fix

Every property in an object type is required unless it has a ?. The literal gives only name, so tsc names the missing one: email. Either give it, { name: "Ada", email: "ada@example.com" }, or, if a user really may have no email, make it optional with email?: string and check it before use.

Using an optional property without a check

function shout(user: { name: string; nickname?: string }) {
  return user.nickname.toUpperCase() + "!";
}

console.log(shout({ name: "Ada" }));

What tsc or Node.js prints

main.ts(2,10): error TS18048: 'user.nickname' is possibly 'undefined'.

Why, and the fix

nickname may be missing, and this very call leaves it out: without tsc, node main.ts stops with TypeError: Cannot read properties of undefined (reading 'toUpperCase'). Decide what happens without a nickname: if (user.nickname === undefined) return user.name + "!"; or use a default, (user.nickname ?? user.name).toUpperCase().

A misspelled property in an object literal

function paint(options: { color: string; width: number }) {
  return options.color + " line, " + options.width + "px";
}

console.log(paint({ colour: "red", width: 2 }));

What tsc or Node.js prints

main.ts(5,21): error TS2561: Object literal may only specify known properties, but 'colour' does not exist in type '{ color: string; width: number; }'. Did you mean to write 'color'?

Why, and the fix

The literal goes straight into the call, so tsc checks every property it lists against the type. colour is not known, and tsc suggests color. Write color: "red". Storing the object in a variable first would silence this check, but paint would still read an undefined color.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

An object type lists properties and their types

An object type describes a value with properties: { title: string; year: number } says the value has a title that is a string and a year that is a number. Write it wherever a type goes, after a variable or a parameter: function describe(book: { title: string; year: number }). Separate the properties with ; or , as you like. tsc then checks both sides. An object that lacks a property is error TS2741 (Property 'year' is missing …), a property of the wrong type is TS2322, and reading a property the type does not list is TS2339, where JavaScript would quietly give undefined. TypeScript compares only the shape: any object with the right properties fits.

Optional properties may be undefined: check first

A ? after the name makes a property optional: { first: string; last?: string }. Callers may leave last out; if they pass it, it must be a string. Inside the function last may be missing, and reading a missing property in JavaScript gives undefined. So TypeScript treats p.last as string | undefined, and with strict on, p.last.toUpperCase() is error TS18048: 'p.last' is possibly 'undefined'. Check before you use it: if (p.last !== undefined) { … }, or p.last?.toUpperCase(), which gives undefined instead of throwing. For a default, p.quantity ?? 1 works; p.quantity || 1 also replaces 0, which is rarely what you want.

Excess property checks catch typos in object literals

An object literal written straight into a place with a declared type, a variable with an annotation or a function argument, gets one more check: every property it lists must be known to the type. paint({ colour: "red", width: 2 }) is error TS2561 when the type says color, with Did you mean to write 'color'?; an extra property with no close match is TS2353. The Handbook calls most of these errors bugs. The check applies only to fresh literals: an object already stored in a variable may carry extra properties, as long as it has everything the type needs. Fix the typo; do not pass the object through a variable just to silence tsc.

Sources

Last reviewed September 30, 2026