CPCODELAB
Practice

Practice TypeScript

You’ve read the lessons — now prove it. Take the quiz for instant feedback, then work the coding exercises. Try each one yourself before you reveal the solution.

Quick quiz

17 questions
Score: 0 / 17
0/17 answered
  1. 1

    Which TypeScript type represents a value that can be either a string or a number?

  2. 2

    What does TypeScript infer as the type of x in const x = [1, 2, 3];?

  3. 3

    What is the key difference between a type alias and an interface in TypeScript?

  4. 4

    Given enum Direction { Up, Down, Left, Right }, what is the value of Direction.Down?

  5. 5

    What does the ? in a function parameter function greet(name?: string) mean?

  6. 6

    What does the readonly modifier do on an interface property?

  7. 7

    What is the output type of this function? function identity<T>(value: T): T { return value; } When called as identity(42), what does TypeScript infer for T?

  8. 8

    Which utility type creates a new type where all properties of T are optional?

  9. 9

    What is a type guard in TypeScript?

  10. 10

    What does Record<string, number> represent?

  11. 11

    Given the code below, which line causes a TypeScript compile error? `` const coords: [number, number] = [10, 20]; // line A coords[0] = 99; // line B coords.push(30); // line C const [x, y] = coords; // line D

  12. 12

    What does keyof T produce when T is { name: string; age: number }?

  13. 13

    Which of these correctly declares a user-defined type guard?

  14. 14

    Given type Box<T> = T extends string ? "text" : "other", what is Box<string | number>?

  15. 15

    What does Awaited<Promise<Promise<number>>> resolve to?

  16. 16

    Which tsconfig flag causes TypeScript to treat arr[i] as T | undefined instead of just T?

  17. 17

    How do you extract the element type from an array type string[] using a conditional type with infer?

🛠️

Coding exercises

8 tasks
1

Type a User Profile

Easy

Define a type alias called UserProfile with the following fields: id (number), username (string), email (string), age (optional number), and role (a string literal union of "admin", "editor", or "viewer"). Then create a variable me of type UserProfile and assign it a valid value.

Starter
ts
// Define your type alias here

// Then create the variable
const me = {
  // ...
};
💡 Show hint

Use type UserProfile = { ... } for the alias and "admin" | "editor" | "viewer" for the union literal.

✅ Show solution
ts
type UserProfile = {
  id: number;
  username: string;
  email: string;
  age?: number;
  role: "admin" | "editor" | "viewer";
};

const me: UserProfile = {
  id: 1,
  username: "jsmith",
  email: "jsmith@example.com",
  role: "editor",
};
2

Generic Identity and Swap

Easy

Write two generic functions: 1. identity<T>(value: T): T — returns its argument unchanged. 2. swap<A, B>(pair: [A, B]): [B, A] — takes a tuple of two elements and returns them in reverse order. Call each function at least once to demonstrate type inference.

Starter
ts
function identity<T>(value: T): T {
  // TODO
}

function swap<A, B>(pair: [A, B]): [B, A] {
  // TODO
}
💡 Show hint

For swap, destructure the tuple: const [a, b] = pair; then return [b, a].

✅ Show solution
ts
function identity<T>(value: T): T {
  return value;
}

function swap<A, B>(pair: [A, B]): [B, A] {
  const [a, b] = pair;
  return [b, a];
}

// Demonstrate inference
const num = identity(42);           // T inferred as number
const str = identity("hello");      // T inferred as string
const swapped = swap(["ts", 100]);  // [number, string]
3

Narrowing with Type Guards

Medium

Write a function formatValue(value: string | number | boolean): string that returns: - Numbers formatted with two decimal places (e.g. 3.14) - Booleans as "yes" or "no" - Strings wrapped in double quotes (e.g. "\"hello\"") Use typeof type guards to narrow the union inside the function.

Starter
ts
function formatValue(value: string | number | boolean): string {
  // TODO: use typeof guards
}
💡 Show hint

Check typeof value === "number", then typeof value === "boolean", and fall through to the string case.

✅ Show solution
ts
function formatValue(value: string | number | boolean): string {
  if (typeof value === "number") {
    return value.toFixed(2);
  }
  if (typeof value === "boolean") {
    return value ? "yes" : "no";
  }
  return '"' + value + '"';
}

console.log(formatValue(3.14159)); // "3.14"
console.log(formatValue(true));    // "yes"
console.log(formatValue("hello")); // "\"hello\""
4

Utility Types Workshop

Medium

You have the following interface: ``ts interface Product { id: number; name: string; price: number; description: string; inStock: boolean; } Using only built-in utility types, derive: 1. ProductPreview — only id, name, and price fields. 2. ProductUpdate — all fields except id, all optional. 3. ProductCatalog — a Record type mapping a string SKU to a Product`. Then declare a variable of each derived type with a valid example value.

Starter
ts
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
}

// Derive types here
type ProductPreview = /* TODO */;
type ProductUpdate  = /* TODO */;
type ProductCatalog = /* TODO */;
💡 Show hint

Use Pick<Product, ...> for ProductPreview, Partial<Omit<Product, "id">> for ProductUpdate, and Record<string, Product> for ProductCatalog.

✅ Show solution
ts
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
}

type ProductPreview = Pick<Product, "id" | "name" | "price">;
type ProductUpdate  = Partial<Omit<Product, "id">>;
type ProductCatalog = Record<string, Product>;

// Example values
const preview: ProductPreview = { id: 1, name: "Widget", price: 9.99 };

const update: ProductUpdate = { price: 7.99, inStock: false };

const catalog: ProductCatalog = {
  "WDG-001": {
    id: 1,
    name: "Widget",
    price: 9.99,
    description: "A small widget",
    inStock: true,
  },
};
5

Generic Stack Data Structure

Hard

Implement a generic Stack<T> class with the following interface: - push(item: T): void — adds an item to the top. - pop(): T | undefined — removes and returns the top item, or undefined if empty. - peek(): T | undefined — returns the top item without removing it. - readonly size: number — the current number of items. - isEmpty(): boolean — returns true when the stack has no items. All internal storage must be private. Demonstrate usage by creating a Stack<string> and a Stack<number>.

Starter
ts
class Stack<T> {
  // TODO: add private storage

  push(item: T): void {
    // TODO
  }

  pop(): T | undefined {
    // TODO
  }

  peek(): T | undefined {
    // TODO
  }

  get size(): number {
    // TODO
    return 0;
  }

  isEmpty(): boolean {
    // TODO
    return true;
  }
}
💡 Show hint

Store items in a private items: T[] = []. push calls this.items.push(item), pop calls this.items.pop(), peek returns this.items[this.items.length - 1], and size returns this.items.length.

✅ Show solution
ts
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

// Usage demonstration
const words = new Stack<string>();
words.push("hello");
words.push("world");
console.log(words.peek());  // "world"
console.log(words.size);    // 2
console.log(words.pop());   // "world"
console.log(words.size);    // 1

const nums = new Stack<number>();
console.log(nums.isEmpty()); // true
nums.push(10);
nums.push(20);
console.log(nums.pop());     // 20
6

Build a Mapped Type Toolkit

Medium

Without using built-in utility types, implement the following mapped types from scratch: 1. MyPartial<T> — makes all properties optional. 2. MyReadonly<T> — makes all properties readonly. 3. MyNullable<T> — makes all properties accept null in addition to their original type. Then apply each to this interface and declare one variable per derived type: ``ts interface Point { x: number; y: number; label: string; }

Starter
ts
// Implement from scratch — do not use Partial / Readonly from the stdlib
type MyPartial<T> = {
  // TODO
};

type MyReadonly<T> = {
  // TODO
};

type MyNullable<T> = {
  // TODO
};

interface Point { x: number; y: number; label: string; }
💡 Show hint

Use [K in keyof T]? for optional, readonly [K in keyof T] for readonly, and T[K] | null for nullable values.

✅ Show solution
ts
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

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

type MyNullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface Point { x: number; y: number; label: string; }

const p1: MyPartial<Point> = { x: 1 };                    // y and label omitted — OK
const p2: MyReadonly<Point> = { x: 0, y: 0, label: "O" }; // cannot reassign any field
const p3: MyNullable<Point> = { x: null, y: 4, label: null };
7

Discriminated Union Result Type

Medium

Model a Result<T, E> type that is either a success or a failure: - Success: { ok: true; value: T } - Failure: { ok: false; error: E } Then write: 1. succeed<T>(value: T): Result<T, never> — wraps a value. 2. fail<E>(error: E): Result<never, E> — wraps an error. 3. unwrap<T, E>(result: Result<T, E>): T — returns the value or throws the error. Demonstrate usage by calling unwrap on both a success and a failure result (the failure case should throw).

Starter
ts
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function succeed<T>(value: T): Result<T, never> {
  // TODO
}

function fail<E>(error: E): Result<never, E> {
  // TODO
}

function unwrap<T, E>(result: Result<T, E>): T {
  // TODO
}
💡 Show hint

Inside unwrap, check result.ok. When true, return result.value. When false, throw result.error (wrap it in an Error if it is a string).

✅ Show solution
ts
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function succeed<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function fail<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

function unwrap<T, E>(result: Result<T, E>): T {
  if (result.ok) {
    return result.value;
  }
  throw result.error instanceof Error
    ? result.error
    : new Error(String(result.error));
}

// Usage
const good = succeed(42);
console.log(unwrap(good)); // 42

const bad = fail("something went wrong");
try {
  unwrap(bad);
} catch (e) {
  console.error((e as Error).message); // "something went wrong"
}
8

Type-Safe Event Emitter with Generics

Hard

Implement a generic EventEmitter<Events> class where Events is a record mapping event names to their payload types. The class must provide: - on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void - off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void - emit<K extends keyof Events>(event: K, payload: Events[K]): void Demonstrate by creating an emitter with events { login: { userId: string }; logout: { userId: string }; error: { message: string } } and subscribing to / emitting each event.

Starter
ts
type ListenerMap<Events> = {
  [K in keyof Events]?: Array<(payload: Events[K]) => void>;
};

class EventEmitter<Events extends Record<string, unknown>> {
  private listeners: ListenerMap<Events> = {};

  on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
    // TODO
  }

  off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
    // TODO
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    // TODO
  }
}
💡 Show hint

In on, initialise this.listeners[event] as an empty array if it does not exist, then push the listener. In off, filter the listener out. In emit, call each stored listener with the payload.

✅ Show solution
ts
type ListenerMap<Events> = {
  [K in keyof Events]?: Array<(payload: Events[K]) => void>;
};

class EventEmitter<Events extends Record<string, unknown>> {
  private listeners: ListenerMap<Events> = {};

  on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    (this.listeners[event] as Array<(p: Events[K]) => void>).push(listener);
  }

  off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
    const arr = this.listeners[event] as Array<(p: Events[K]) => void> | undefined;
    if (arr) {
      this.listeners[event] = arr.filter((l) => l !== listener) as typeof arr;
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const arr = this.listeners[event] as Array<(p: Events[K]) => void> | undefined;
    arr?.forEach((l) => l(payload));
  }
}

// Usage
type AppEvents = {
  login: { userId: string };
  logout: { userId: string };
  error: { message: string };
};

const emitter = new EventEmitter<AppEvents>();

function onLogin(payload: { userId: string }) {
  console.log("User logged in:", payload.userId);
}

emitter.on("login", onLogin);
emitter.on("error", (p) => console.error("Error:", p.message));

emitter.emit("login", { userId: "u_001" });  // "User logged in: u_001"
emitter.emit("error", { message: "Disk full" });

emitter.off("login", onLogin);
emitter.emit("logout", { userId: "u_001" }); // no listeners — silent