Warm-up · Activity 1 of 7
Warm-up from JavaScript: name has a default value. What does this print?
function greet(name = "guest") {
return "Hello, " + name;
}
console.log(greet(), greet(undefined), greet(null));// B4.2 · ~30 min · Beginner
After this lesson you can write functions that take optional, default and any number of arguments, spread arrays into calls, and destructure a typed parameter.
You will be able to
Warm-up · Activity 1 of 7
function greet(name = "guest") {
return "Hello, " + name;
}
console.log(greet(), greet(undefined), greet(null));Predict · Activity 2 of 7
function greet(name?: string): string {
return "Hello, " + name.toUpperCase();
}
console.log(greet());Practice · Activity 3 of 7
function sum(____values: number[]): number {
let total = 0;
for (const v of values) total += v;
return total;
}
console.log(sum(3, 4.5, 2));Practice · Activity 4 of 7
function pad(text: string, width?: number): string {
// what is width here?
return text.padStart(width ?? 8);
}Practice · Activity 5 of 7
function point(x: number, y: number): string {
return "(" + x + ", " + y + ")";
}
const a = [3, 4];
const b = [3, 4] as const;
console.log(point(...a));
console.log(point(...b));Brain teaser · Activity 6 of 7
function repeat(text: string, times = 2): string {
return text.repeat(times);
}
const counts = [undefined, 0, 3];
console.log(counts.map((n) => "[" + repeat("ab", n) + "]").join(" "));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
euro has a default currency, line destructures an Item whose quantity is optional, printReceipt and sum take any number of arguments, and thanks has an optional name. The last two lines spread a tuple made with as const into a function with two fixed parameters. Run npx tsc, then node main.ts. Then remove as const and run tsc again.
main.ts
// A receipt: optional, default and rest parameters, and a destructured parameter.
type Item = { name: string; price: number; quantity?: number };
function euro(cents: number, symbol = "EUR"): string {
return (cents / 100).toFixed(2) + " " + symbol; // symbol is a string, from its default
}
function line({ name, price, quantity }: Item): string {
const count = quantity ?? 1; // quantity is number | undefined here
return (count + " x " + name).padEnd(16) + euro(price * count);
}
function sum(...amounts: number[]): number {
let total = 0;
for (const amount of amounts) total += amount;
return total;
}
function printReceipt(title: string, ...items: Item[]): void {
console.log(title);
for (const item of items) console.log(" " + line(item));
const cents = items.map((item) => item.price * (item.quantity ?? 1));
console.log(" " + "total".padEnd(16) + euro(sum(...cents)));
}
function thanks(name?: string): string {
return name === undefined ? "Thank you!" : "Thank you, " + name + "!";
}
function clock(hours: number, minutes: number): string {
return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0");
}
const order: Item[] = [
{ name: "coffee", price: 320, quantity: 2 },
{ name: "croissant", price: 245 },
];
printReceipt("Cafe Mitte", ...order, { name: "water", price: 150 });
console.log(thanks());
console.log(thanks("Ada"));
const opens = [7, 30] as const; // a tuple of exactly two numbers
console.log("Open daily from " + clock(...opens));
Run it with
npx tsc
node main.tsOutput
Cafe Mitte
2 x coffee 6.40 EUR
1 x croissant 2.45 EUR
1 x water 1.50 EUR
total 10.35 EUR
Thank you!
Thank you, Ada!
Open daily from 07:30Tab 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(1250) should give 12.50 EUR, but all three parameters are required, so tsc reports error TS2554: Expected 3 arguments, but got 1. Give currency the default "EUR" and decimals the default 2, without annotations. Then add a fourth, optional parameter label: when it is given, the result starts with the label and a colon, as in Total: 12.50 EUR.
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.
A default goes after the parameter name with =, and TypeScript infers the type from it: currency = "EUR".
An optional parameter has a ? after its name and must come after the required ones: label?: string.
Inside, label may be undefined: return label === undefined ? text : label + ": " + text;
One way to solve it. Yours can look different and still pass the checks.
export function formatPrice(cents: number, currency = "EUR", decimals = 2, label?: string): string {
const text = (cents / 100).toFixed(decimals) + " " + currency;
return label === undefined ? text : label + ": " + text;
}
console.log(formatPrice(1250));
console.log(formatPrice(1999, "USD", 0));
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 formatPrice(cents: number, currency: string, decimals: number): string {
return (cents / 100).toFixed(decimals) + " " + currency;
}
console.log(formatPrice(1250));
console.log(formatPrice(1999, "USD", 0));
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatPrice} from './main.ts';
test('formatPrice(1250) uses EUR and two decimals', () => {
assert.equal(formatPrice(1250), '12.50 EUR', `formatPrice(1250) gave ${JSON.stringify(formatPrice(1250))}`);
});
test('a given currency and number of decimals are used', () => {
assert.equal(formatPrice(1999, 'USD', 0), '20 USD', `formatPrice(1999, 'USD', 0) gave ${JSON.stringify(formatPrice(1999, 'USD', 0))}`);
});
test('undefined as the currency gives the default', () => {
assert.equal(formatPrice(1250, undefined, 1), '12.5 EUR', `formatPrice(1250, undefined, 1) gave ${JSON.stringify(formatPrice(1250, undefined, 1))}`);
});
test('a label goes in front, with a colon', () => {
assert.equal(formatPrice(1250, 'EUR', 2, 'Total'), 'Total: 12.50 EUR', `formatPrice(1250, 'EUR', 2, 'Total') gave ${JSON.stringify(formatPrice(1250, 'EUR', 2, '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.jsExercise 2 of 2
total should add the points of any number of rounds, passed one by one: total(a, b), total(), or total(...list). describe has no parameter type, so tsc reports error TS7006. Make rounds a rest parameter, and change describe to destructure player and points from a Round in its parameter list, with the annotation after the pattern and the return type string.
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.
A rest parameter has three dots before its name and an array type: ...rounds: Round[].
The body of total can stay as it is: rounds is still an array, empty when no rounds are passed.
export function describe({ player, points }: Round): string { return player + ": " + points; }
One way to solve it. Yours can look different and still pass the checks.
export type Round = { player: string; points: number };
export function total(...rounds: Round[]): number {
let sum = 0;
for (const round of rounds) sum += round.points;
return sum;
}
export function describe({ player, points }: Round): string {
return player + ": " + points;
}
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 type Round = { player: string; points: number };
export function total(rounds: Round[]): number {
let sum = 0;
for (const round of rounds) sum += round.points;
return sum;
}
export function describe(round) {
return round.player + ": " + round.points;
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {total, describe} from './main.ts';
const a = {player: 'Ada', points: 7};
const b = {player: 'Ada', points: 5};
test('total adds rounds passed one by one', () => {
assert.equal(total(a, b), 12, 'total(a, b) should be 12');
});
test('total() with no rounds is 0', () => {
assert.equal(total(), 0, 'total() should be 0');
});
test('an array can be spread into total', () => {
const list = [a, b, {player: 'Grace', points: 9}];
assert.equal(total(...list), 21, 'total(...list) should be 21');
});
test('describe names the player and the points', () => {
assert.equal(describe(a), 'Ada: 7', `describe(a) gave ${JSON.stringify(describe(a))}`);
});
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.jsfunction label(prefix?: string, name: string): string {
return (prefix ?? "") + name;
}
console.log(label(undefined, "Ada"));
What tsc or Node.js prints
main.ts(1,33): error TS1016: A required parameter cannot follow an optional parameter.Why, and the fix
Arguments are matched by position, so a caller cannot leave out the first and pass the second. Put the required parameters first: label(name: string, prefix?: string). If prefix must stay first, give it a default instead (prefix = ""), and callers pass undefined to use it.
function area({ width: number, height: number }) {
return width * height;
}
console.log(area({ width: 3, height: 4 }));
What tsc or Node.js prints
main.ts(1,24): error TS2300: Duplicate identifier 'number'.Why, and the fix
In a destructuring pattern, width: number means "take width and call the variable number", so there are two variables named number and none named width. The annotation goes after the whole pattern: function area({ width, height }: { width: number; height: number }).
function myForEach(arr: number[], callback: (arg: number, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed());
});
What tsc or Node.js prints
main.ts(8,15): error TS18048: 'i' is possibly 'undefined'.Why, and the fix
index?: number says myForEach might call the callback without an index, so every callback must cope with undefined, though the loop always passes one. Follow the Handbook's rule and write index: number. Callbacks that do not need it, such as (a) => console.log(a), still fit.
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.