Warm-up · Activity 1 of 7
Warm-up from JavaScript: neither function returns a value. What does this print?
function noop() {
return;
}
const values = [noop(), [1, 2].forEach((n) => n * 2)];
console.log(values);// B4.4 · ~30 min · Beginner
After this lesson you can type what a function returns when it returns nothing useful, accept any object or value safely, and declare what this is.
You will be able to
Warm-up · Activity 1 of 7
function noop() {
return;
}
const values = [noop(), [1, 2].forEach((n) => n * 2)];
console.log(values);Predict · Activity 2 of 7
type VoidFunc = () => void;
const f1: VoidFunc = () => true;
function f2(): void {
return true;
}Practice · Activity 3 of 7
type Listener = (event: string) => ____;
const seen: string[] = [];
const listeners: Listener[] = [(e) => console.log("got " + e), (e) => seen.push(e)];
listeners.forEach((listener) => listener("save"));
console.log(seen.length);Practice · Activity 4 of 7
function keys(o: object): string[] {
return Object.keys(o);
}
function keys2(o: Object): string[] {
return Object.keys(o);
}
console.log(keys({ a: 1 }), keys([5]), keys(() => 1));
console.log(keys2(42));
console.log(keys(42));Practice · Activity 5 of 7
const counter = {
count: 0,
increment(this: { count: number }) {
this.count++;
return this.count;
},
};
console.log(counter.increment());
const inc = counter.increment;
inc();Brain teaser · Activity 6 of 7
const user = {
name: "Ada",
greet() {
return "Hi, " + this.name;
},
};
const greet = user.greet;
console.log(user.greet());
console.log(greet());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
The Handbook's DB, as an interface: filterUsers calls its callback with a User as this, and the callback is a function expression that declares this: User. forEach takes a void callback, and push's number is ignored. safeParse returns unknown, so the program checks the value before reading theme. countKeys takes an object. Run npx tsc, then node main.ts. Then replace the callback with () => this.admin and run tsc again.
main.ts
interface User {
name: string;
admin: boolean;
}
// filter is called with a User as this: declare it, as the Handbook's DB does.
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const users: User[] = [
{ name: "Ada", admin: true },
{ name: "Grace", admin: false },
{ name: "Linus", admin: true },
];
const db: DB = {
filterUsers(filter) {
return users.filter((user) => filter.call(user));
},
};
// A function expression, not an arrow: an arrow would not get this from the call.
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
// A void callback may return something: push returns a number, which forEach ignores.
const names: string[] = [];
admins.forEach((user) => names.push(user.name));
console.log("admins: " + names.join(", "));
// unknown: the caller must check the value before using it.
function safeParse(s: string): unknown {
return JSON.parse(s);
}
const settings = safeParse('{"theme": "dark"}');
if (typeof settings === "object" && settings !== null && "theme" in settings) {
console.log("theme: " + String(settings.theme));
}
// object accepts arrays and functions too, but no primitives.
function countKeys(value: object): number {
return Object.keys(value).length;
}
console.log(countKeys({ a: 1, b: 2 }), countKeys(["x"]), countKeys(() => 0));
Run it with
npx tsc
node main.tsOutput
admins: Ada, Linus
theme: dark
2 1 0Tab 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
A Listener receives a message. The two listeners at the bottom push into history, and push returns a number, so tsc reports error TS2322: Type 'number' is not assignable to type 'undefined'. for each of them. Change Listener so that any listener fits, whatever it returns. Give on the return type void. Then make emit return how many listeners it called, with the return type number.
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.
Which return type does the Handbook use for a callback whose result is ignored?
export type Listener = (message: string) => void; then push's number is simply ignored.
In emit, after the forEach: return listeners.length; and write : number after the parameter list.
One way to solve it. Yours can look different and still pass the checks.
export type Listener = (message: string) => void;
const listeners: Listener[] = [];
export const history: string[] = [];
export function on(listener: Listener): void {
listeners.push(listener);
}
export function emit(message: string): number {
listeners.forEach((listener) => listener(message));
return listeners.length;
}
on((message) => history.push(message));
on((message) => history.push(message.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 type Listener = (message: string) => undefined;
const listeners: Listener[] = [];
export const history: string[] = [];
export function on(listener: Listener) {
listeners.push(listener);
}
export function emit(message: string) {
listeners.forEach((listener) => listener(message));
}
on((message) => history.push(message));
on((message) => history.push(message.toUpperCase()));
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {on, emit, history} from './main.ts';
test('emit calls both listeners and returns 2', () => {
const called = emit('hi');
assert.equal(called, 2, `emit('hi') returned ${called}`);
});
test('the listeners wrote the message into history', () => {
assert.deepEqual(history.slice(0, 2), ['hi', 'HI'], `history starts with ${JSON.stringify(history.slice(0, 2))}`);
});
test('a listener added later is called too', () => {
const got = [];
on((message) => got.push(message));
const called = emit('ok');
assert.equal(called, 3, `after adding a third listener, emit returned ${called}`);
assert.deepEqual(got, ['ok'], `the new listener received ${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
clickTwice takes increment off the counter and calls it on its own, so this is undefined and the tests stop with a TypeError. tsc does not notice, because increment does not declare this. Declare it in the Counter interface: increment(this: Counter): number. Run the tests: tsc now reports error TS2684 in clickTwice. Fix clickTwice so that it calls increment as a method of counter, twice, and returns the second result.
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.
this is the first parameter in the signature, and it is not a real parameter: increment(this: Counter): number;.
The object literal in makeCounter needs no change: it gets its this from the Counter return type.
Call the method with its object: counter.increment(); then return counter.increment();.
One way to solve it. Yours can look different and still pass the checks.
export interface Counter {
count: number;
step: number;
increment(this: Counter): number;
}
export function makeCounter(step: number): Counter {
return {
count: 0,
step,
increment() {
this.count += this.step;
return this.count;
},
};
}
export function clickTwice(counter: Counter): number {
counter.increment();
return counter.increment();
}
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 Counter {
count: number;
step: number;
increment(): number;
}
export function makeCounter(step: number): Counter {
return {
count: 0,
step,
increment() {
this.count += this.step;
return this.count;
},
};
}
export function clickTwice(counter: Counter): number {
const click = counter.increment;
click();
return click();
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {makeCounter, clickTwice} from './main.ts';
test('increment adds the step', () => {
const counter = makeCounter(2);
const result = counter.increment();
assert.equal(result, 2, `makeCounter(2).increment() returned ${result}`);
});
test('clickTwice returns the count after two clicks', () => {
const result = clickTwice(makeCounter(5));
assert.equal(result, 10, `clickTwice(makeCounter(5)) returned ${result}`);
});
test('clickTwice changes the counter it was given', () => {
const counter = makeCounter(3);
clickTwice(counter);
assert.equal(counter.count, 6, `after clickTwice, count is ${counter.count}`);
});
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 save(name: string): void {
console.log("saved " + name);
return name.length;
}
What tsc or Node.js prints
main.ts(3,3): error TS2322: Type 'number' is not assignable to type 'void'.Why, and the fix
A function whose own return type is void must not return a value; only a function stored in a void function type may. If callers need the length, say so: function save(name: string): number. If not, delete the return statement.
function runLater(f: () => void) {
return f();
}
const r = runLater(() => 42);
console.log(r.toFixed());
What tsc or Node.js prints
main.ts(5,15): error TS2339: Property 'toFixed' does not exist on type 'void'.Why, and the fix
f has the type () => void, so whatever it returns is treated as void, even though this arrow function returns 42. That is the point of void: the caller promises not to use the result. If runLater should pass the result on, type the callback with it: f: () => number, and give runLater the return type number.
interface User {
name: string;
admin: boolean;
}
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const users: User[] = [
{ name: "Ada", admin: true },
{ name: "Grace", admin: false },
];
const db: DB = {
filterUsers(filter) {
return users.filter((user) => filter.call(user));
},
};
const admins = db.filterUsers(() => this.admin);
What tsc or Node.js prints
main.ts(21,37): error TS2532: Object is possibly 'undefined'.Why, and the fix
An arrow function has no this of its own; it uses the this around it, and outside any function in main.ts there is none. filterUsers sets this for its callback only if the callback is a function expression. The Handbook: "you need to use function and not arrow functions". Write db.filterUsers(function (this: User) { return this.admin; }).
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.