Warm-up · Activity 1 of 7
// 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.
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
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);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 }) {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()); }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));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 }));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.tsOutput
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
Hint 1
An object type goes after the parameter name, like any annotation: book: { … }.
Hint 2
Inside the braces, list each property with its type, separated by ;: title: string; author: string; year: number.
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 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
Hint 1
discount has the type number | undefined. Handle the undefined case first and return early.
Hint 2
if (!product.discount) also catches 0, which the third test passes on purpose. Compare with undefined instead.
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsCommon 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.