Warm-up · Activity 1 of 7
// B2.2 · ~30 min · Beginner
Annotations versus inference
After this lesson you can annotate variables, parameters and return types, predict what TypeScript infers without them, and leave out the annotations that only repeat it.
You will be able to
- Annotate variables, parameters and return types, and say where each annotation goes
- Predict the type TypeScript infers from a value, a mixed array and a function's return statements
- Recognise contextual typing in callbacks, and decide when an annotation is needed
Predict · Activity 2 of 7
Predict before you read on: which type does TypeScript infer for mixed?
const mixed = [1, "two", 3];Practice · Activity 3 of 7
Fill in the return type, so that tsc accepts the function and it prints 3.5.
function average(a: number, b: number): ____ { return (a + b) / 2; } console.log(average(3, 4));function average(a: number, b: number): {Practice · Activity 4 of 7
The callback has no annotation. Which type does TypeScript infer for lengths?
const names = ["Ada", "Grace"]; const lengths = names.map((name) => name.length);Practice · Activity 5 of 7
The return type says string. What does tsc report?
function grade(points: number): string { if (points >= 50) { return "pass"; } }Brain teaser · Activity 6 of 7
Brain teaser. Both lines hold the same arrow function (n) => n * 2, and neither n has an annotation. Which lines does tsc report?
const double = (n) => n * 2; console.log([1, 2, 3].map((n) => n * 2));Apply · Activity 7 of 7
Mini-task. Start from const words = ["tree", "sky", "river"]. Keep the words longer than 3 letters with filter, turn them into capitals with map, and join them with a function joinAll(items: string[]): string. Annotate only what needs it: joinAll's parameter and return type. The callbacks and the variables get no annotation. Run npx tsc and node main.ts; it prints TREE, RIVER. Then misspell toUpperCase in the map callback and read the error: contextual typing knew word was a string.
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
An order total, half annotated
The comments say which types are written and which TypeScript infers. The parameters of the two declared functions are annotated; prices, label, the return type of sum and the callback's price are inferred. Run npx tsc, which prints nothing, then node main.ts. Then try label = 42 or prices.push("free"): tsc reports both, although neither variable has an annotation.
main.ts
// Which types are written, and which does TypeScript infer?
const prices = [4.5, 12, 3.25]; // inferred: number[]
const discount: number = 0.1; // annotated, though 0.1 would give number anyway
let label = "Order"; // inferred: string
function applyDiscount(amount: number, rate: number): number {
return amount * (1 - rate);
}
// No return type written: TypeScript infers number from the return statement.
function sum(values: number[]) {
let total = 0;
for (const value of values) {
total += value;
}
return total;
}
// price needs no annotation: map passes the elements of a number[].
const discounted = prices.map((price) => applyDiscount(price, discount));
label = label + " #42";
console.log(label);
console.log("before:", sum(prices));
console.log("after:", sum(discounted).toFixed(2));
Run it with
npx tsc
node main.tsOutput
Order #42
before: 19.75
after: 17.78- prices is number[] and label is string, both inferred from their first values.
- sum has no return type, yet sum(discounted).toFixed(2) type-checks: TypeScript inferred number.
- price in the map callback is a number from its context, so applyDiscount(price, discount) is checked.
- The discount annotation changes nothing; it only repeats what 0.1 would give.
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
Annotate only what needs it
Both functions work, but tsc reports four times Parameter … implicitly has an 'any' type. Annotate the three parameters of the declared functions: values is a list of numbers, words a list of text, minLength a number. Do not annotate the callback's word: once words has its type, the fourth error disappears by itself. Return types are optional here.
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
A list of numbers is number[], a list of text is string[].
Hint 2
The callback (word) => … gets its type from words.filter: once words is a string[], word is a string.
Hint 3
The first lines become average(values: number[]) and longWords(words: string[], minLength: number).
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export function average(values: number[]): number {
let sum = 0;
for (const value of values) {
sum += value;
}
return sum / values.length;
}
export function longWords(words: string[], minLength: number): string[] {
return words.filter((word) => word.length >= minLength);
}
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 average(values) {
let sum = 0;
for (const value of values) {
sum += value;
}
return sum / values.length;
}
export function longWords(words, minLength) {
return words.filter((word) => word.length >= minLength);
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {average, longWords} from './main.ts';
test('average([2, 4, 9]) is 5', () => {
assert.equal(average([2, 4, 9]), 5, 'average([2, 4, 9]) should be 5');
});
test('longWords keeps the words with at least 3 letters', () => {
assert.deepEqual(longWords(['a', 'tree', 'sky'], 3), ['tree', 'sky'], 'longWords(["a", "tree", "sky"], 3) should be ["tree", "sky"]');
});
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
Let the return type find the bug
formatScore should always return text: "50% (pass)" from 50 percent up, and for example "25% (fail)" below. Its return type is annotated as string, and tsc reports the line that breaks that promise. Fix that line so that it returns the percentage followed by % (fail). Leave the annotations as they are.
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
tsc reports main.ts(6,3): error TS2322: Type 'number' is not assignable to type 'string'. Line 6 returns the bare number.
Hint 2
Build the text the same way as on line 4, with toFixed(0) and a suffix.
Hint 3
Line 6 becomes return percent.toFixed(0) + "% (fail)";
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export function formatScore(points: number, max: number): string {
const percent = (points / max) * 100;
if (percent >= 50) {
return percent.toFixed(0) + "% (pass)";
}
return percent.toFixed(0) + "% (fail)";
}
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 formatScore(points: number, max: number): string {
const percent = (points / max) * 100;
if (percent >= 50) {
return percent.toFixed(0) + "% (pass)";
}
return percent;
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatScore} from './main.ts';
test('20 of 40 is a pass', () => {
assert.equal(formatScore(20, 40), '50% (pass)', 'formatScore(20, 40) should be "50% (pass)"');
});
test('10 of 40 is a fail', () => {
assert.equal(formatScore(10, 40), '25% (fail)', `formatScore(10, 40) should be "25% (fail)", got ${JSON.stringify(formatScore(10, 40))}`);
});
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
An arrow function with no context
const double = (n) => n * 2;
console.log(double(21));
What tsc or Node.js prints
main.ts(1,17): error TS7006: Parameter 'n' implicitly has an 'any' type.Why, and the fix
Contextual typing only works where the function is passed somewhere that says what its parameters are, such as map or forEach. A function stored in a variable has no such context, so TypeScript cannot infer n, and strict reports it. Annotate the parameter: const double = (n: number) => n * 2. The return type is then inferred as number.
Doing arithmetic with an element of a mixed array
const scores = [90, "85", 70];
const doubled = scores[1] * 2;
console.log(doubled);
What tsc or Node.js prints
main.ts(2,17): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.Why, and the fix
The best common type of 90, "85" and 70 is (string | number)[], so every element might be text, and * needs a number on the left. JavaScript would quietly print 170. Fix the data, not the check: write 85 without quotes, and scores becomes number[]. If the text really comes from outside, convert it once with Number("85").
A body that breaks its return type
function isAdult(age: number): boolean {
return age >= 18 ? "yes" : "no";
}
console.log(isAdult(20));
What tsc or Node.js prints
main.ts(2,22): error TS2322: Type 'string' is not assignable to type 'boolean'.Why, and the fix
The annotation promises a boolean, and tsc checks each value returned against it: "yes" (column 22) and "no" (column 30) are both reported. Without the annotation, TypeScript would infer a string return type and callers would get "yes" instead of true. Return the comparison itself: return age >= 18;
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.