Warm-up · Activity 1 of 7
Warm-up from JavaScript: functions are values that you can pass to other functions. What does this print?
function twice(fn, x) {
return fn(fn(x));
}
const addThree = (n) => n + 3;
console.log(twice(addThree, 1));// B4.1 · ~30 min · Beginner
After this lesson you can write and name the type of a function, tell which functions fit it, and type a function with properties or a constructor.
Lesson 1 of 5 in B4 More on functions
You will be able to
Warm-up · Activity 1 of 7
function twice(fn, x) {
return fn(fn(x));
}
const addThree = (n) => n + 3;
console.log(twice(addThree, 1));Predict · Activity 2 of 7
type Handler = (string) => void;
const shout: Handler = (text) => console.log(text.toUpperCase());Practice · Activity 3 of 7
type Formatter = (value: number) ____ string;
const euro: Formatter = (value) => value.toFixed(2) + " EUR";
console.log(euro(4.5));Practice · Activity 4 of 7
type Logger = (message: string, level: number) => void;
const a: Logger = (message, level) => console.log(level, message);
const b: Logger = (message) => console.log(message);
const c: Logger = () => console.log("log");
const d: Logger = (message: string, level: number, time: number) => console.log(time);Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
type Check = (value: number) => boolean;
function isBig(amount: number): boolean {
return amount > 100;
}
const checks: Check[] = [isBig, (n) => n % 2 === 0, () => true];
console.log(checks.map((check) => check(150)).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
A price in cents goes through a list of steps, each a PriceStep. The discount is also a Discount: a function with a label, described by a call signature. addShipping and log get their parameter types from the type they are stored in, and log has fewer parameters than Logger, which is allowed. Run npx tsc, then node main.ts. Then delete the line tenPercentOff.label = "10% off" and run tsc again.
main.ts
// A checkout: a price in cents goes through a list of steps.
type PriceStep = (cents: number) => number;
// A discount is a step with a label: a call signature plus a property.
type Discount = {
label: string;
(cents: number): number;
};
function tenPercentOff(cents: number): number {
return Math.round(cents * 0.9);
}
tenPercentOff.label = "10% off";
const addShipping: PriceStep = (cents) => cents + 490; // cents is a number, from PriceStep
type Logger = (message: string, step: number) => void;
const log: Logger = (message) => console.log(" " + message); // fewer parameters fit
function euro(cents: number): string {
return (cents / 100).toFixed(2) + " EUR";
}
function checkout(cents: number, steps: PriceStep[], logger: Logger): number {
let total = cents;
steps.forEach((step, i) => {
total = step(total);
logger("after step " + (i + 1) + ": " + euro(total), i + 1);
});
return total;
}
function describe(d: Discount, cents: number): string {
return d.label + " on " + euro(cents) + " saves " + euro(cents - d(cents));
}
console.log(describe(tenPercentOff, 4999));
console.log("checkout:");
const total = checkout(4999, [tenPercentOff, addShipping], log);
console.log("total: " + euro(total));
Run it with
npx tsc
node main.tsOutput
10% off on 49.99 EUR saves 5.00 EUR
checkout:
after step 1: 44.99 EUR
after step 2: 49.89 EUR
total: 49.89 EURTab 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
countWhere counts the items for which test returns true, and isEven is one such test. tsc reports error TS7006 for test and for n. Write an exported type alias NumberTest for a function that takes a number and returns a boolean, and use it for the parameter test and for isEven, so that n gets its type from NumberTest. Give countWhere the return type number. 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.
A function type is written like an arrow function: (parameter: type) => return type.
export type NumberTest = (value: number) => boolean; then test: NumberTest.
For isEven, annotate the const, not the arrow function: export const isEven: NumberTest = (n) => n % 2 === 0;
One way to solve it. Yours can look different and still pass the checks.
export type NumberTest = (value: number) => boolean;
export function countWhere(items: number[], test: NumberTest): number {
let count = 0;
for (const item of items) {
if (test(item)) count++;
}
return count;
}
export const isEven: NumberTest = (n) => n % 2 === 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 countWhere(items: number[], test) {
let count = 0;
for (const item of items) {
if (test(item)) count++;
}
return count;
}
export const isEven = (n) => n % 2 === 0;
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countWhere, isEven} from './main.ts';
test('isEven tells even numbers from odd ones', () => {
assert.equal(isEven(4), true, 'isEven(4) should be true');
assert.equal(isEven(7), false, 'isEven(7) should be false');
});
test('countWhere counts the even numbers in [1, 2, 3, 4]', () => {
assert.equal(countWhere([1, 2, 3, 4], isEven), 2, 'countWhere([1, 2, 3, 4], isEven) should be 2');
});
test('countWhere works with an arrow function as the test', () => {
assert.equal(countWhere([5, 12, 20], (n) => n > 10), 2, 'countWhere([5, 12, 20], (n) => n > 10) should be 2');
assert.equal(countWhere([], isEven), 0, 'countWhere of an empty list should be 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.jsExercise 2 of 2
A Tally is a function that adds an amount and returns the new total, and it also has a total property. makeTally keeps the total in a local variable instead, so tsc reports error TS2741: Property 'total' is missing in type '(amount: number) => number' but required in type 'Tally'. Keep the Tally type as it is. Change makeTally so that the function it returns has a total property that starts at 0 and grows with every call.
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 function in JavaScript can have properties. TypeScript lets you add one to a const function right after it is declared.
Write add.total = 0; after the arrow function and before return add;.
Inside add, use add.total instead of the local variable: add.total += amount; return add.total;
One way to solve it. Yours can look different and still pass the checks.
export type Tally = {
total: number;
(amount: number): number;
};
export function makeTally(): Tally {
const add = (amount: number): number => {
add.total += amount;
return add.total;
};
add.total = 0;
return add;
}
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 Tally = {
total: number;
(amount: number): number;
};
export function makeTally(): Tally {
let total = 0;
const add = (amount: number): number => {
total += amount;
return total;
};
return add;
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeTally} from './main.ts';
test('a new tally starts at 0', () => {
const tally = makeTally();
assert.equal(tally.total, 0, `a new tally's total is ${tally.total}`);
});
test('calling the tally adds to its total', () => {
const tally = makeTally();
tally(5);
const result = tally(3);
assert.equal(result, 8, `tally(5) then tally(3) returned ${result}`);
assert.equal(tally.total, 8, `after tally(5) and tally(3) the total is ${tally.total}`);
});
test('two tallies count separately', () => {
const a = makeTally();
const b = makeTally();
a(10);
assert.equal(b.total, 0, `after a(10), b's total is ${b.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.jstype Handler = (string) => void;
const shout: Handler = (text) => console.log(text.toUpperCase());
shout("hello");
What tsc or Node.js prints
main.ts(1,17): error TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?Why, and the fix
In a function type every parameter needs a name, so string is read as the name of a parameter of type any. Write type Handler = (text: string) => void. The name is only for reading: a function whose parameter has another name still fits.
type Discount = {
label: string;
(cents: number) => number;
};
function tenOff(cents: number): number {
return Math.round(cents * 0.9);
}
tenOff.label = "10% off";
const d: Discount = tenOff;
console.log(d.label, d(2000));
What tsc or Node.js prints
main.ts(3,19): error TS1005: ':' expected.Why, and the fix
In an object type, a call signature puts a colon between the parameter list and the return type: (cents: number): number;. The arrow belongs to a function type expression on its own, such as type PriceStep = (cents: number) => number.
type OnSave = (name: string) => void;
const onSave: OnSave = (name: string, when: number) => console.log(name, when);
onSave("report.pdf");
What tsc or Node.js prints
main.ts(3,7): error TS2322: Type '(name: string, when: number) => void' is not assignable to type 'OnSave'.Why, and the fix
Whoever calls an OnSave passes one argument, so when would always be undefined; the second line of the message says Target signature provides too few arguments. Either drop the parameter, or, if the caller really has a time to pass, add it to the type: type OnSave = (name: string, when: number) => void.
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.