Warm-up · Activity 1 of 7
Warm-up from JavaScript destructuring: which defaults are used? What does this print?
const { a = 1, b = 2, c = 3 } = { a: undefined, b: null };
console.log(a, b, c);// B5.1 · ~30 min · Beginner
After this lesson you can give optional properties defaults where you destructure them, mark properties readonly, and say what readonly protects and what it does not.
Lesson 1 of 5 in B5 Object types
You will be able to
Warm-up · Activity 1 of 7
const { a = 1, b = 2, c = 3 } = { a: undefined, b: null };
console.log(a, b, c);Predict · Activity 2 of 7
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
home.resident.age++;
}
function evict(home: Home) {
home.resident = { name: "Victor the Evictor", age: 42 };
}Practice · Activity 3 of 7
interface PaintOptions {
shape: string;
xPos?: number;
yPos?: number;
}
function paintShape({ shape, xPos ____, yPos = 0 }: PaintOptions) {
console.log(shape + " at " + xPos + "," + yPos);
}
paintShape({ shape: "circle" });
paintShape({ shape: "square", xPos: 100 });Practice · Activity 4 of 7
interface Settings {
theme?: string;
fontSize?: number;
}
function describe(settings: Settings) {
const { theme, fontSize = 14 } = settings;
// here
}Practice · Activity 5 of 7
interface UserDefaults {
colorThemeOverride?: "dark" | "light";
}
const settings: UserDefaults = {};
settings.colorThemeOverride = "dark";
settings.colorThemeOverride = undefined;
delete settings.colorThemeOverride;Brain teaser · Activity 6 of 7
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = { name: "Person McPersonface", age: 42 };
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age);
writablePerson.age++;
console.log(readonlyPerson.age);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
describeOrder destructures its parameter and gives quantity and giftWrap defaults, so the body needs no undefined checks. id and customer are readonly. The program then changes the customer's city, which readonly allows, because it is only one level deep. Run npx tsc, then node main.ts. Then add order.id = "X"; or order.customer = order.customer; and run tsc again.
main.ts
// An order: optional settings get defaults, the id and the customer are readonly.
interface Customer {
name: string;
city: string;
}
interface Order {
readonly id: string;
readonly customer: Customer;
quantity?: number;
giftWrap?: boolean;
}
// Defaults in the pattern: in the body, quantity is a number and giftWrap a boolean.
function describeOrder({ id, customer, quantity = 1, giftWrap = false }: Order): string {
const wrap = giftWrap ? ", gift-wrapped" : "";
return id + ": " + quantity + " for " + customer.name + " in " + customer.city + wrap;
}
const order: Order = { id: "A-17", customer: { name: "Ada", city: "Bonn" } };
console.log(describeOrder(order));
// readonly is shallow: order.customer cannot point elsewhere, but its city can change.
order.customer.city = "Berlin";
console.log(describeOrder({ ...order, quantity: 3, giftWrap: true }));
// An explicit undefined gets the default too.
console.log(describeOrder({ id: "B-2", customer: order.customer, quantity: undefined }));
Run it with
npx tsc
node main.tsOutput
A-17: 1 for Ada in Bonn
A-17: 3 for Ada in Berlin, gift-wrapped
B-2: 1 for Ada in BerlinTab 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.
Exercise 1 of 2
formatPrice should give 4.50 EUR for { amount: 4.5 }: currency defaults to eur, written in capitals, and decimals to 2. tsc reports error TS18048: 'options.currency' is possibly 'undefined'. Destructure the parameter instead, and give currency and decimals their defaults in the pattern. A decimals of 0 must stay 0. 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.
Replace the parameter options: PriceOptions with a pattern: ({ amount, currency, decimals }: PriceOptions).
A default goes after the name inside the pattern, with =: currency = "eur".
Use the variables in the body: amount.toFixed(decimals) + " " + currency.toUpperCase().
One way to solve it. Yours can look different and still pass the checks.
export interface PriceOptions {
amount: number;
currency?: string;
decimals?: number;
}
export function formatPrice({ amount, currency = "eur", decimals = 2 }: PriceOptions): string {
return amount.toFixed(decimals) + " " + currency.toUpperCase();
}
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 interface PriceOptions {
amount: number;
currency?: string;
decimals?: number;
}
export function formatPrice(options: PriceOptions): string {
return options.amount.toFixed(options.decimals) + " " + options.currency.toUpperCase();
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatPrice} from './main.ts';
test('both defaults are used', () => {
const got = formatPrice({amount: 4.5});
assert.equal(got, '4.50 EUR', `formatPrice({amount: 4.5}) gave ${JSON.stringify(got)}`);
});
test('a given currency and decimals are used', () => {
const got = formatPrice({amount: 3, currency: 'usd', decimals: 1});
assert.equal(got, '3.0 USD', `formatPrice({amount: 3, currency: 'usd', decimals: 1}) gave ${JSON.stringify(got)}`);
});
test('decimals: 0 stays 0', () => {
const got = formatPrice({amount: 7, decimals: 0});
assert.equal(got, '7 EUR', `formatPrice({amount: 7, decimals: 0}) gave ${JSON.stringify(got)}`);
});
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
An order must keep its id and its customer object; other parts of the shop hold the same customer object and must see a new city. forbiddenChanges proves the first part: each // @ts-expect-error expects an error on the next line, and tsc reports error TS2578: Unused '@ts-expect-error' directive. while the writes still compile. Mark id and customer readonly. Then fix moveCustomer, which tsc now rejects: change the city of the customer object the order already has.
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.
readonly goes before the property name in the interface: readonly id: string;
After that, order.customer = { … } in moveCustomer is error TS2540. readonly is shallow: the city inside customer can still change.
The body of moveCustomer becomes order.customer.city = city;
One way to solve it. Yours can look different and still pass the checks.
export interface Order {
readonly id: string;
readonly customer: { name: string; city: string };
total: number;
}
export function moveCustomer(order: Order, city: string): void {
order.customer.city = city;
}
export function addToTotal(order: Order, amount: number): void {
order.total += amount;
}
function forbiddenChanges(order: Order) {
// @ts-expect-error: an order keeps its id
order.id = "X-0";
// @ts-expect-error: an order keeps its customer object
order.customer = { name: "Mallory", city: "Nowhere" };
}
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 interface Order {
id: string;
customer: { name: string; city: string };
total: number;
}
export function moveCustomer(order: Order, city: string): void {
order.customer = { name: order.customer.name, city };
}
export function addToTotal(order: Order, amount: number): void {
order.total += amount;
}
function forbiddenChanges(order: Order) {
// @ts-expect-error: an order keeps its id
order.id = "X-0";
// @ts-expect-error: an order keeps its customer object
order.customer = { name: "Mallory", city: "Nowhere" };
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {moveCustomer, addToTotal} from './main.ts';
test('moveCustomer sets the new city', () => {
const order = {id: 'A-1', customer: {name: 'Ada', city: 'Bonn'}, total: 10};
moveCustomer(order, 'Berlin');
assert.equal(order.customer.city, 'Berlin', `the city is ${order.customer.city}`);
});
test('moveCustomer keeps the same customer object', () => {
const customer = {name: 'Ada', city: 'Bonn'};
const order = {id: 'A-1', customer, total: 10};
moveCustomer(order, 'Berlin');
assert.ok(order.customer === customer, 'order.customer was replaced by a new object');
assert.equal(customer.city, 'Berlin', `the shared customer object still says ${customer.city}`);
});
test('addToTotal adds to the total', () => {
const order = {id: 'A-1', customer: {name: 'Ada', city: 'Bonn'}, total: 10};
addToTotal(order, 2.5);
assert.equal(order.total, 12.5, `the total is ${order.total}`);
});
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.jsinterface Config {
readonly apiUrl: string;
retries: number;
}
const config: Config = { apiUrl: "https://example.com", retries: 3 };
config.retries = 5;
config.apiUrl = "http://localhost";
What tsc or Node.js prints
main.ts(8,8): error TS2540: Cannot assign to 'apiUrl' because it is a read-only property.Why, and the fix
The type says apiUrl is set once, when the object is created, and never changed. If another URL is needed, create a new object: const local: Config = { ...config, apiUrl: "http://localhost" }. If it really has to change in place, the property should not be readonly: remove the modifier rather than working around it.
interface Settings {
theme?: string;
fontSize?: number;
}
function describe(settings: Settings) {
const { theme, fontSize = 14 } = settings;
return theme.toUpperCase() + " " + fontSize + "px";
}
What tsc or Node.js prints
main.ts(8,10): error TS18048: 'theme' is possibly 'undefined'.Why, and the fix
Destructuring does not remove undefined: theme is string | undefined, as settings.theme would be. fontSize is a number only because it has a default. Give theme one too, const { theme = "light", fontSize = 14 } = settings, or check it for undefined before calling toUpperCase.
interface Options {
host?: string;
port?: number;
}
function connect({ host = "localhost", port = 80 }: Options) {
return host + ":" + port;
}
console.log(connect());
What tsc or Node.js prints
main.ts(10,13): error TS2554: Expected 1 arguments, but got 0.Why, and the fix
The defaults belong to the properties of the object, but the object itself is still a required parameter, and destructuring undefined would throw a TypeError. Give the whole parameter a default as well: function connect({ host = "localhost", port = 80 }: Options = {}). Then connect() uses an empty object, and both property defaults apply.
TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.