Warm-up · Activity 1 of 7
Warm-up from JavaScript: an object used as a dictionary. What does this print?
const stock = {};
stock["apples"] = 3;
stock.pears = 2;
console.log(stock.apples + stock["pears"], stock.plums);// B5.2 · ~30 min · Beginner
After this lesson you can type objects whose keys are not known in advance, mix them with named properties, and handle missing keys.
You will be able to
Warm-up · Activity 1 of 7
const stock = {};
stock["apples"] = 3;
stock.pears = 2;
console.log(stock.apples + stock["pears"], stock.plums);Predict · Activity 2 of 7
const prices = { apple: 0.5, pear: 0.75 };
const item: string = "apple";
console.log(prices[item]);Practice · Activity 3 of 7
interface Scores {
[name: ____]: number;
}
const scores: Scores = { Ada: 91, Grace: 78 };
scores.Linus = 85;
console.log(scores.Ada + scores.Grace + scores.Linus);Practice · Activity 4 of 7
interface NumberOrStringDictionary {
____
length: number;
name: string;
}Practice · Activity 5 of 7
interface ReadonlyStringArray {
readonly [index: number]: string;
}
const myArray: ReadonlyStringArray = ["Alice", "Bob"];
console.log(myArray[0]);
myArray[2] = "Mallory";Brain teaser · Activity 6 of 7
interface Scores {
[name: string]: number;
}
const scores: Scores = { Ada: 91, Grace: 78 };
const missing = scores["Zoe"];
console.log(missing);
console.log(missing.toFixed(1));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
Scores maps any student name to a number. ClassInfo has two named properties next to its index signature, so the index type is a union that both fit. GradeLimits is readonly. Object.entries walks the keys, and scoreOf checks with in before it reads a key that may be missing. Run npx tsc, then node main.ts. Then add limits.D = 50; or a named property active: boolean to ClassInfo, and run tsc again.
main.ts
// Scores per student: the names are not known when the type is written.
interface Scores {
[student: string]: number;
}
// A named property must fit the index type, so the index type is a union.
interface ClassInfo {
[key: string]: string | number;
name: string;
size: number;
}
// readonly: the grade limits can be read, not changed.
interface GradeLimits {
readonly [grade: string]: number;
}
const scores: Scores = { Ada: 91, Grace: 78 };
scores["Linus"] = 64;
const info: ClassInfo = { name: "7b", size: 3, room: "B12" };
const limits: GradeLimits = { A: 90, B: 75, C: 60 };
function gradeOf(score: number): string {
for (const [grade, min] of Object.entries(limits)) {
if (score >= min) return grade;
}
return "F";
}
for (const [student, score] of Object.entries(scores)) {
console.log(student + ": " + score + " (" + gradeOf(score) + ")");
}
// A key nobody set is undefined at run time, whatever the type says: check first.
function scoreOf(student: string): string {
if (!(student in scores)) return student + ": no score";
return student + ": " + scores[student];
}
console.log(scoreOf("Zoe"));
console.log("class " + info.name + " in room " + info.room);
Run it with
npx tsc
node main.tsOutput
Ada: 91 (A)
Grace: 78 (B)
Linus: 64 (C)
Zoe: no score
class 7b in room B12Tab 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
countVotes counts how often each option was chosen, but counts = {} has no keys, so tsc reports error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. Export an interface VoteCounts with a string index signature whose values are numbers. Annotate counts with it, and give countVotes the return type VoteCounts. 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.
An index signature goes inside the interface: [option: string]: number;
Write export interface VoteCounts { … } above the function, then const counts: VoteCounts = {};
The return type goes after the parameter list: countVotes(votes: string[]): VoteCounts {
One way to solve it. Yours can look different and still pass the checks.
export interface VoteCounts {
[option: string]: number;
}
export function countVotes(votes: string[]): VoteCounts {
const counts: VoteCounts = {};
for (const vote of votes) {
counts[vote] = (counts[vote] ?? 0) + 1;
}
return counts;
}
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 countVotes(votes: string[]) {
const counts = {};
for (const vote of votes) {
counts[vote] = (counts[vote] ?? 0) + 1;
}
return counts;
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countVotes} from './main.ts';
test('each option is counted', () => {
const got = countVotes(['tea', 'coffee', 'tea']);
assert.deepEqual(got, {tea: 2, coffee: 1}, `countVotes gave ${JSON.stringify(got)}`);
});
test('no votes give an empty object', () => {
const got = countVotes([]);
assert.deepEqual(got, {}, `countVotes([]) gave ${JSON.stringify(got)}`);
});
test('an option that got no vote is missing', () => {
const got = countVotes(['tea']);
assert.equal(got['water'], undefined, `water should be missing, but is ${got['water']}`);
});
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
Messages holds texts by key, plus a version number. tsc reports error TS2411: Property 'version' of type 'number' is not assignable to 'string' index type 'string'., and the // @ts-expect-error in noEdits is unused, because messages can still be changed. Make the index type a union that version fits, and make the index signature readonly. Then fix translate: it returns the text in capitals, or the key in brackets, such as [missing], when there is no text for the key, also for version.
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.
The index type must allow both texts and the version: readonly [key: string]: string | number;
messages[key] is then string | number, and at run time it may be undefined. typeof text !== "string" catches a number and undefined at once.
const text = messages[key]; if (typeof text !== "string") return "[" + key + "]"; return text.toUpperCase();
One way to solve it. Yours can look different and still pass the checks.
export interface Messages {
readonly [key: string]: string | number;
version: number;
}
export const en: Messages = { version: 2, greeting: "Hello", farewell: "Goodbye" };
export function translate(messages: Messages, key: string): string {
const text = messages[key];
if (typeof text !== "string") return "[" + key + "]";
return text.toUpperCase();
}
function noEdits(messages: Messages) {
// @ts-expect-error: messages are read-only
messages["greeting"] = "Hi";
}
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 Messages {
[key: string]: string;
version: number;
}
export const en: Messages = { version: 2, greeting: "Hello", farewell: "Goodbye" };
export function translate(messages: Messages, key: string): string {
return messages[key].toUpperCase();
}
function noEdits(messages: Messages) {
// @ts-expect-error: messages are read-only
messages["greeting"] = "Hi";
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {en, translate} from './main.ts';
test('a known key gives the text in capitals', () => {
const got = translate(en, 'greeting');
assert.equal(got, 'HELLO', `translate(en, 'greeting') gave ${JSON.stringify(got)}`);
});
test('a missing key gives the key in brackets', () => {
const got = translate(en, 'missing');
assert.equal(got, '[missing]', `translate(en, 'missing') gave ${JSON.stringify(got)}`);
});
test('version is not a text', () => {
const got = translate(en, 'version');
assert.equal(got, '[version]', `translate(en, 'version') 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.jsinterface Stock {
[product: string]: number;
updated: string;
}
What tsc or Node.js prints
main.ts(3,3): error TS2411: Property 'updated' of type 'string' is not assignable to 'string' index type 'number'.Why, and the fix
The index signature says every key gives a number, and updated is a key too: stock["updated"] must be a number. Either widen the index type, [product: string]: number | string, which makes every product read as number | string, or keep the counts in their own property: { updated: string; counts: { [product: string]: number } }.
const prices = { apple: 0.5, pear: 0.75 };
const item: string = "apple";
console.log(prices[item]);
What tsc or Node.js prints
main.ts(3,13): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ apple: number; pear: number; }'.Why, and the fix
prices has two known keys, and item may be any string, so tsc cannot say what prices[item] is. If the keys are open, say so: const prices: { [item: string]: number } = { … }, and check for missing keys. If item is always apple or pear, give it that type: const item: "apple" | "pear" = "apple".
interface Limits {
readonly [grade: string]: number;
}
const limits: Limits = { A: 90, B: 75 };
console.log(limits.A);
limits.C = 60;
What tsc or Node.js prints
main.ts(7,1): error TS2542: Index signature in type 'Limits' only permits reading.Why, and the fix
readonly applies to every key the signature covers, including keys not there yet: limits.C = 60 adds one, and that is a write. Give C its value when the object is created, { A: 90, B: 75, C: 60 }, or create a new object: const more: Limits = { ...limits, C: 60 }.
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.