TypeScript Exercises
Practise what you've learned. The first section has fill-in-the-blank exercises: type the missing piece into each highlighted gap and press Check. The second section has small coding problems to solve in the editor, each with a worked solution.
Fill in the blanks
Basics
Annotate the variable so it can only hold text.
let city: = "Lisbon";
The primitive type for text is lowercase string — see lesson 03.
Give scores a type: an array of numbers.
const scores: = [];
Either number[] or Array<number> works — see lesson 06.
Complete the tuple type for a [name, age] pair.
type Person = [string, ]; const p: Person = ["Ana", 31];
A tuple lists one type per position in square brackets — see lesson 07.
Which special type forces you to check a value before using it?
function parse(input: string): {
return JSON.parse(input);
}
unknown accepts anything but can't be used until narrowed — see lesson 05.
Mark the function as one that never returns normally.
function fail(message: string): {
throw new Error(message);
}
A function that always throws has return type never — see lesson 05.
Objects and unions
Make email optional.
interface User {
name: string;
email: string;
}
A ? after the property name makes it optional — see lesson 08.
Make id impossible to change after creation.
interface Product {
id: number;
name: string;
}
readonly allows the property to be set once and never reassigned.
Allow only these three values.
type Theme = "light" "dark" | "system";
A union of string literals uses | between members — see lesson 11.
Combine both types into one that has all their properties.
type Timestamped = { createdAt: Date };
type Post = { title: string } Timestamped;
An intersection type uses & — see lesson 12.
Fall back to 20 only when the setting is null or undefined (not when it's 0).
const pageSize = setting 20;
The nullish coalescing operator ?? ignores 0 and "" — see lesson 13.
Read city safely even if address is missing.
const city = user.addresscity;
Optional chaining ?. returns undefined instead of throwing.
Functions and classes
Give the parameter a default value of "Hello".
function greet(name: string, greeting ) {
return `${greeting}, ${name}`;
}
A default value is written with =; its type is inferred — see lesson 15.
Accept any number of numeric arguments.
function sum(: number[]) {
return values.reduce((a, b) => a + b, 0);
}
A rest parameter uses ... and has an array type.
Restrict balance so only code inside the class can access it.
class Account {
balance = 0;
}
private members are only reachable from inside the class — see lesson 17.
Prevent this class from being instantiated directly.
class Shape {
abstract area(): number;
}
An abstract class can only be extended — see lesson 18.
Make the class promise to provide everything in Printable.
class Invoice Printable {
print() { return "INV-1"; }
}
implements makes the compiler check the class against the interface.
Generics and type-level code
Declare a type parameter so the function returns the same type it receives.
function identity(value: T): T {
return value;
}
Type parameters go in angle brackets before the parameter list — see lesson 20.
Constrain T so it must have a numeric length.
function longest<T { length: number }>(a: T, b: T) {
return a.length >= b.length ? a : b;
}
extends constrains a type parameter — see lesson 21.
Produce a union of the property names of Settings.
type SettingName = Settings;
keyof gives a union of property names — see lesson 24.
Turn this function into a type guard.
function isString(value: unknown): value string {
return typeof value === "string";
}
A return type of value is T is a type predicate — see lesson 23.
Build a type with every property of Profile optional.
function update(changes: <Profile>) {}
Partial<T> makes all properties optional — see lesson 26.
Remove password from the User type.
type PublicUser = <User, "password">;
Omit keeps every property except the ones listed.
Get the type a function returns.
type Session = <typeof createSession>;
ReturnType extracts a function's return type — see lesson 27.
Complete the mapped type that makes every property readonly.
type Frozen<T> = { readonly [K keyof T]: T[K] };
Mapped types loop over keys with in — see lesson 28.
Extract the element type of an array type.
type ElementOf<T> = T extends ( E)[] ? E : never;
infer captures part of a type inside a conditional type — see lesson 29.
Modules and tooling
Import User only as a type, so it's removed from the compiled output.
import { User } from "./models";
import type is always erased — see lesson 31.
Turn on the family of strict checks in tsconfig.json.
{ "compilerOptions": { "": true } }
strict enables noImplicitAny, strictNullChecks and more — see lesson 38.
Check a value matches a type while keeping its precise inferred type.
const palette = { primary: "#d6336c" } Record<string, string>;
satisfies validates without widening — see lesson 14.
Coding problems
Solve each problem in the editor, then open the solution to compare. Remember that the in-page editor runs your code but doesn't show type errors — paste your answer into a real editor or the official Playground to check the types too.
1. Typed word counter
Write countWords(text: string): Record<string, number> that returns how many times each lowercase word appears.
Show solution
function countWords(text: string): Record<string, number> { const counts: Record<string, number> = {}; for (const word of text.toLowerCase().split(/\s+/).filter(Boolean)) { counts[word] = (counts[word] ?? 0) + 1; } return counts; } console.log(countWords("the cat and the hat"));
{ the: 2, cat: 1, and: 1, hat: 1 }2. Safe first element
Write a generic first function that returns the first element of any array, or undefined if the array is empty. The return type must match the element type.
Show solution
function first<T>(items: readonly T[]): T | undefined { return items[0]; } console.log(first([3, 1, 2])); console.log(first(["x"])); console.log(first([]));
3 x undefined
Using readonly T[] means the function also accepts readonly arrays, and promises callers it won't modify their data.
3. Shape areas with a discriminated union
Define a Shape union of circles (radius), rectangles (width, height) and triangles (base, height), and an area function that handles every case with an exhaustive switch.
Show solution
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number } | { kind: "triangle"; base: number; height: number }; function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "rect": return s.width * s.height; case "triangle": return (s.base * s.height) / 2; default: { const unreachable: never = s; return unreachable; } } } const shapes: Shape[] = [ { kind: "circle", radius: 1 }, { kind: "rect", width: 2, height: 3 }, { kind: "triangle", base: 4, height: 5 }, ]; shapes.forEach(s => console.log(s.kind, area(s).toFixed(2)));
circle 3.14 rect 6.00 triangle 10.00
4. Type-safe property picker
Write pluck(items, key) that returns an array of one property from every object. pluck(users, "name") should be typed string[], and a key that doesn't exist should be a compile error.
Show solution
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] { return items.map(item => item[key]); } const users = [ { name: "Ana", age: 31 }, { name: "Ben", age: 25 }, ]; console.log(pluck(users, "name")); console.log(pluck(users, "age"));
[ 'Ana', 'Ben' ] [ 31, 25 ]
5. Validate untrusted JSON
Write a type guard isProduct(value: unknown): value is Product for { id: number; name: string; tags: string[] }, and use it to keep only valid items from a parsed JSON array.
Show solution
interface Product { id: number; name: string; tags: string[]; } function isProduct(value: unknown): value is Product { if (typeof value !== "object" || value === null) return false; const v = value as Record<string, unknown>; return ( typeof v.id === "number" && typeof v.name === "string" && Array.isArray(v.tags) && v.tags.every(t => typeof t === "string") ); } const raw: unknown = JSON.parse('[{"id":1,"name":"Lamp","tags":["home"]},{"id":"2","name":"Bad"},{"id":3,"name":"Desk","tags":[]}]'); const products = Array.isArray(raw) ? raw.filter(isProduct) : []; console.log(products.map(p => p.name));
[ 'Lamp', 'Desk' ]
6. Typed event emitter
Build a small Emitter<Events> class where Events maps event names to payload types, so on("login", cb) gives cb the right payload type and emitting an unknown event is an error.
Show solution
type AppEvents = { login: { user: string }; logout: undefined; }; class Emitter<Events extends Record<string, unknown>> { private handlers: { [E in keyof Events]?: ((payload: Events[E]) => void)[] } = {}; on<E extends keyof Events>(event: E, handler: (payload: Events[E]) => void) { (this.handlers[event] ??= []).push(handler); } emit<E extends keyof Events>(event: E, payload: Events[E]) { this.handlers[event]?.forEach(h => h(payload)); } } const bus = new Emitter<AppEvents>(); bus.on("login", p => console.log(`welcome ${p.user}`)); bus.on("logout", () => console.log("bye")); bus.emit("login", { user: "Ana" }); bus.emit("logout", undefined);
welcome Ana bye
This combines generics (lesson 20), constraints (21), keyof and indexed access (24) and a mapped type (28).