Warm-up · Activity 1 of 7
// B2.5 · ~38 min · Beginner
Literal types and as const; build: a typed contact book
After this lesson you can allow only a few exact values, predict when TypeScript widens a literal, and build a typed contact book.
Lesson 5 of 5 in B2 Everyday types
You will be able to
- Write literal types and unions of literals, and read the errors tsc reports for other values
- Predict when TypeScript widens a literal type, and keep it with as "GET" or as const
- Build a typed contact book with an interface, an optional property, a literal union and an as const list
Predict · Activity 2 of 7
Predict before you read on: alignment accepts only three exact strings. What does tsc report?
function printText(s: string, alignment: "left" | "right" | "center") { console.log(s + " (" + alignment + ")"); } printText("Hello, world", "left"); printText("G'day, mate", "centre");Practice · Activity 3 of 7
Fill in the two words after the object literal that keep method as the literal type "GET", so that tsc accepts the call.
function handleRequest(url: string, method: "GET" | "POST") { console.log(method + " " + url); } const req = { url: "https://example.com", method: "GET" } ____; handleRequest(req.url, req.method);const req = { url: "https://example.com", method: "GET" } ;Practice · Activity 4 of 7
Which type does TypeScript infer for greeting?
const greeting = "Hello"; let other = "Hello";Practice · Activity 5 of 7
Match each declaration to the type TypeScript infers.
Brain teaser · Activity 6 of 7
Brain teaser. settings is a const. What does tsc report for line 2?
const settings = { theme: "dark" }; settings.theme = "light"; console.log(settings.theme);Apply · Activity 7 of 7
Mini-task. Declare type Theme = "light" | "dark" and a function background(theme: Theme) that returns "#222222" for dark and "#ffffff" otherwise. Store a saved setting as an object { user: "ada", theme: "dark" } and pass saved.theme to background. Read the error tsc gives first, then fix it with as const. Finally try background("blue").
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
A price list with aligned columns
pad accepts only "left" or "right" as its alignment. Each column's settings are stored in an object with as const, so align keeps its literal type and can be passed to pad. total is a let, so TypeScript infers number and it can grow in the loop. Run npx tsc, then node main.ts. Then remove one as const and read what tsc says, or pass "middle" to pad.
main.ts
// Literal types: only these exact values fit.
type Align = "left" | "right";
function pad(text: string, width: number, align: Align): string {
return align === "left" ? text.padEnd(width, ".") : text.padStart(width, ".");
}
// as const keeps "left" and "right" as literal types, not string.
const nameColumn = { width: 10, align: "left" } as const;
const priceColumn = { width: 8, align: "right" } as const;
function row(name: string, price: string): string {
return pad(name, nameColumn.width, nameColumn.align) + pad(price, priceColumn.width, priceColumn.align);
}
const items = [
{ name: "Tea", price: 2.5 },
{ name: "Cake", price: 3.75 },
];
let total = 0; // let: TypeScript infers number, so it can change
for (const item of items) {
console.log(row(item.name, item.price.toFixed(2)));
total += item.price;
}
console.log(row("Total", total.toFixed(2)));
Run it with
npx tsc
node main.tsOutput
Tea...........2.50
Cake..........3.75
Total.........6.25- Without as const, nameColumn.align would be string, and pad(…, nameColumn.align) would be error TS2345.
- items has no annotation: TypeScript infers { name: string; price: number; }[] from the literals.
- The === comparison inside pad is checked as well: align === "centre" would be error TS2367.
- The literal types exist only for tsc: Node.js strips them, and the output is what plain JavaScript prints.
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 3
Let tsc catch the typo
This program should print Express: 9.90 EUR, but it prints 4.90, and tsc reports nothing: speed is a string, so the typo "expres" is accepted and quietly treated as standard. Change the type of speed to a union of the two literal strings "standard" and "express". Then tsc reports the typo: fix 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.
Hints
Hint 1
A union of literals is written with | between the values: "a" | "b".
Hint 2
The first line becomes export function shippingCost(speed: "standard" | "express"): number {
Hint 3
tsc then reports Argument of type '"expres"' on line 5. Write "express".
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export function shippingCost(speed: "standard" | "express"): number {
return speed === "express" ? 9.9 : 4.9;
}
console.log("Express: " + shippingCost("express").toFixed(2) + " EUR");
console.log("Standard: " + shippingCost("standard").toFixed(2) + " EUR");
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 shippingCost(speed: string): number {
return speed === "express" ? 9.9 : 4.9;
}
console.log("Express: " + shippingCost("expres").toFixed(2) + " EUR");
console.log("Standard: " + shippingCost("standard").toFixed(2) + " EUR");
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {shippingCost} from './main.ts';
test('shippingCost("express") is 9.9', () => {
assert.equal(shippingCost('express'), 9.9, 'shippingCost("express") should be 9.9');
});
test('shippingCost("standard") is 4.9', () => {
assert.equal(shippingCost('standard'), 4.9, 'shippingCost("standard") should be 4.9');
});
test('the program prints both prices', async () => {
const out = (await runMain()).trimEnd();
assert.equal(out, 'Express: 9.90 EUR\nStandard: 4.90 EUR', `the program printed ${JSON.stringify(out)}`);
});
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 3
Keep the method literal
request accepts only "GET" or "POST", and the program prints the right lines, but tsc reports Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"' twice. Fix it where the objects are created, not in the calls and not in the function: keep each method as its literal type.
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
Object properties are widened: home.method is string, not "GET", because it could still be assigned.
Hint 2
Two words after the closing brace of an object literal turn every property into its literal type.
Hint 3
Write const home = { url: "/home", method: "GET" } as const; and the same for login.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export function request(url: string, method: "GET" | "POST"): string {
return method + " " + url;
}
const home = { url: "/home", method: "GET" } as const;
const login = { url: "/login", method: "POST" } as const;
console.log(request(home.url, home.method));
console.log(request(login.url, login.method));
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 request(url: string, method: "GET" | "POST"): string {
return method + " " + url;
}
const home = { url: "/home", method: "GET" };
const login = { url: "/login", method: "POST" };
console.log(request(home.url, home.method));
console.log(request(login.url, login.method));
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {request} from './main.ts';
test('request joins method and url', () => {
assert.equal(request('/cart', 'POST'), 'POST /cart', 'request("/cart", "POST") should be POST /cart');
});
test('the program prints both requests', async () => {
const out = (await runMain()).trimEnd();
assert.equal(out, 'GET /home\nPOST /login', `the program printed ${JSON.stringify(out)}`);
});
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 3 of 3
Build: a typed contact book
The module build. The contact book below runs, but tsc reports many errors and the functions are empty. Declare type Kind ("friend", "work" or "family") and interface Contact (name and email as strings, an optional phone, kind as a Kind, favorite as a boolean). Make KINDS an as const list and give ada a type, so that tsc accepts them. Then write the functions: formatContact gives * Ada Lovelace <ada@example.com> (work, no phone), with "* " only for favorites and the phone number when there is one; byKind keeps the contacts of one kind; favorites gives their names. The tests check each function and the printed book.
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
Start with the types: type Kind = "friend" | "work" | "family"; and an interface Contact with phone?: string.
Hint 2
ada's kind is widened to string, and so is each element of KINDS. Write const ada: Contact = …, and put as const after the KINDS array.
Hint 3
contact.phone ?? "no phone" gives the default. byKind is book.filter((contact) => contact.kind === kind); favorites filters on favorite, then maps to the name.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// A typed contact book. Declare the types, then finish the functions.
type Kind = "friend" | "work" | "family";
interface Contact {
name: string;
email: string;
phone?: string;
kind: Kind;
favorite: boolean;
}
export const KINDS = ["friend", "work", "family"] as const;
export function formatContact(contact: Contact): string {
const star = contact.favorite ? "* " : "";
const phone = contact.phone ?? "no phone";
return star + contact.name + " <" + contact.email + "> (" + contact.kind + ", " + phone + ")";
}
export function byKind(book: Contact[], kind: Kind): Contact[] {
return book.filter((contact) => contact.kind === kind);
}
export function favorites(book: Contact[]): string[] {
return book.filter((contact) => contact.favorite).map((contact) => contact.name);
}
export function summary(book: Contact[]): string {
const parts: string[] = [];
for (const kind of KINDS) {
parts.push(kind + ": " + byKind(book, kind).length);
}
return parts.join(", ");
}
const ada: Contact = { name: "Ada Lovelace", email: "ada@example.com", kind: "work", favorite: true };
const book: Contact[] = [
ada,
{ name: "Grace Hopper", email: "grace@example.com", phone: "+49 30 1234567", kind: "work", favorite: false },
{ name: "Linus Torvalds", email: "linus@example.com", kind: "friend", favorite: true },
];
for (const contact of book) {
console.log(formatContact(contact));
}
console.log(summary(book));
console.log("Favorites: " + favorites(book).join(", "));
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
// A typed contact book. Declare the types, then finish the functions.
// 1. Kind: "friend", "work" or "family".
// 2. Contact: name and email (text), phone (optional text), kind (a Kind),
// favorite (true or false).
export const KINDS = ["friend", "work", "family"];
export function formatContact(contact: Contact): string {
// "* Ada Lovelace <ada@example.com> (work, no phone)"; only favorites start with "* "
return "";
}
export function byKind(book: Contact[], kind: Kind): Contact[] {
return [];
}
export function favorites(book: Contact[]): string[] {
// the names of every favorite, in book order
return [];
}
export function summary(book: Contact[]): string {
// "friend: 1, work: 2, family: 0", in the order of KINDS
const parts: string[] = [];
for (const kind of KINDS) {
parts.push(kind + ": " + byKind(book, kind).length);
}
return parts.join(", ");
}
const ada = { name: "Ada Lovelace", email: "ada@example.com", kind: "work", favorite: true };
const book: Contact[] = [
ada,
{ name: "Grace Hopper", email: "grace@example.com", phone: "+49 30 1234567", kind: "work", favorite: false },
{ name: "Linus Torvalds", email: "linus@example.com", kind: "friend", favorite: true },
];
for (const contact of book) {
console.log(formatContact(contact));
}
console.log(summary(book));
console.log("Favorites: " + favorites(book).join(", "));
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {formatContact, byKind, favorites, summary} from './main.ts';
const ada = {name: 'Ada', email: 'ada@example.com', kind: 'work', favorite: true};
const bob = {name: 'Bob', email: 'bob@example.com', phone: '555-0100', kind: 'family', favorite: false};
const cy = {name: 'Cy', email: 'cy@example.com', kind: 'work', favorite: false};
const book = [ada, bob, cy];
test('formatContact marks a favorite without a phone', () => {
const line = formatContact(ada);
assert.equal(line, '* Ada <ada@example.com> (work, no phone)', `formatContact(ada) gave ${JSON.stringify(line)}`);
});
test('formatContact shows the phone of a contact who is not a favorite', () => {
const line = formatContact(bob);
assert.equal(line, 'Bob <bob@example.com> (family, 555-0100)', `formatContact(bob) gave ${JSON.stringify(line)}`);
});
test('byKind keeps only the contacts of that kind', () => {
const names = byKind(book, 'work').map((contact) => contact.name);
assert.deepEqual(names, ['Ada', 'Cy'], `byKind(book, 'work') gave ${JSON.stringify(names)}`);
assert.equal(byKind(book, 'friend').length, 0, 'byKind(book, "friend") should be empty');
});
test('favorites gives the names of the favorites', () => {
const names = favorites(book);
assert.deepEqual(names, ['Ada'], `favorites(book) gave ${JSON.stringify(names)}`);
});
test('summary counts every kind', () => {
const text = summary(book);
assert.equal(text, 'friend: 0, work: 2, family: 1', `summary(book) gave ${JSON.stringify(text)}`);
});
test('the program prints the whole book', async () => {
const out = (await runMain()).trimEnd();
assert.equal(
out,
'* Ada Lovelace <ada@example.com> (work, no phone)\nGrace Hopper <grace@example.com> (work, +49 30 1234567)\n* Linus Torvalds <linus@example.com> (friend, no phone)\nfriend: 1, work: 2, family: 0\nFavorites: Ada Lovelace, Linus Torvalds',
`the program printed ${JSON.stringify(out)}`
);
});
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
A value just outside the literal union
function printText(s: string, alignment: "left" | "right" | "center") {
console.log(s);
}
printText("Price list", "centre");
What tsc or Node.js prints
main.ts(5,25): error TS2345: Argument of type '"centre"' is not assignable to parameter of type '"center" | "left" | "right"'.Why, and the fix
A literal union allows exactly the values it lists, spelled exactly as listed. tsc names the value it found, "centre", and the values it accepts. Write "center". This is the point of a literal union: in plain JavaScript the typo would run and quietly fall through every check for "center".
A property widened to string
type Mode = "light" | "dark";
let mode: Mode = "light";
const saved = { mode: "dark" };
mode = saved.mode;
What tsc or Node.js prints
main.ts(5,1): error TS2322: Type 'string' is not assignable to type 'Mode'.Why, and the fix
saved.mode holds "dark", but its type is string: an object property can still be assigned, so TypeScript widens it. string is wider than Mode, so tsc refuses. Keep the literal where the object is created: const saved = { mode: "dark" } as const;, or annotate it: const saved: { mode: Mode } = { mode: "dark" };.
Changing a list made with as const
const KINDS = ["friend", "work"] as const;
KINDS.push("family");
What tsc or Node.js prints
main.ts(2,7): error TS2339: Property 'push' does not exist on type 'readonly ["friend", "work"]'.Why, and the fix
as const makes an array a readonly tuple: exactly these elements, in this order, and no method that changes it, such as push. That is what you want for a fixed list of kinds. If the list must grow, add "family" to the literal itself, or leave out as const and annotate it, for example const kinds: string[] = ["friend", "work"].
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.