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";

Give scores a type: an array of numbers.

const scores:  = [];

Complete the tuple type for a [name, age] pair.

type Person = [string, ];
const p: Person = ["Ana", 31];

Which special type forces you to check a value before using it?

function parse(input: string):  {
  return JSON.parse(input);
}

Mark the function as one that never returns normally.

function fail(message: string):  {
  throw new Error(message);
}

Objects and unions

Make email optional.

interface User {
  name: string;
  email: string;
}

Make id impossible to change after creation.

interface Product {
   id: number;
  name: string;
}

Allow only these three values.

type Theme = "light"  "dark" | "system";

Combine both types into one that has all their properties.

type Timestamped = { createdAt: Date };
type Post = { title: string }  Timestamped;

Fall back to 20 only when the setting is null or undefined (not when it's 0).

const pageSize = setting  20;

Read city safely even if address is missing.

const city = user.addresscity;

Functions and classes

Give the parameter a default value of "Hello".

function greet(name: string, greeting ) {
  return `${greeting}, ${name}`;
}

Accept any number of numeric arguments.

function sum(: number[]) {
  return values.reduce((a, b) => a + b, 0);
}

Restrict balance so only code inside the class can access it.

class Account {
   balance = 0;
}

Prevent this class from being instantiated directly.

 class Shape {
  abstract area(): number;
}

Make the class promise to provide everything in Printable.

class Invoice  Printable {
  print() { return "INV-1"; }
}

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;
}

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;
}

Produce a union of the property names of Settings.

type SettingName =  Settings;

Turn this function into a type guard.

function isString(value: unknown): value  string {
  return typeof value === "string";
}

Build a type with every property of Profile optional.

function update(changes: <Profile>) {}

Remove password from the User type.

type PublicUser = <User, "password">;

Get the type a function returns.

type Session = <typeof createSession>;

Complete the mapped type that makes every property readonly.

type Frozen<T> = { readonly [K  keyof T]: T[K] };

Extract the element type of an array type.

type ElementOf<T> = T extends ( E)[] ? E : never;

Modules and tooling

Import User only as a type, so it's removed from the compiled output.

import  { User } from "./models";

Turn on the family of strict checks in tsconfig.json.

{ "compilerOptions": { "": true } }

Check a value matches a type while keeping its precise inferred type.

const palette = { primary: "#d6336c" }  Record<string, string>;

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.

Try it yourself
Console output
Show solution
TS solution.ts
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"));
Output
{ 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.

Try it yourself
Console output
Show solution
TS solution.ts
function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

console.log(first([3, 1, 2]));
console.log(first(["x"]));
console.log(first([]));
Output
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.

Try it yourself
Console output
Show solution
TS solution.ts
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)));
Output
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.

Try it yourself
Console output
Show solution
TS solution.ts
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"));
Output
[ '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.

Try it yourself
Console output
Show solution
TS solution.ts
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));
Output
[ '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.

Try it yourself
Console output
Show solution
TS solution.ts
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);
Output
welcome Ana
bye

This combines generics (lesson 20), constraints (21), keyof and indexed access (24) and a mapped type (28).