Skip to content
Laviyx Learn

Try

TypeScript Unions, Narrowing and Interfaces Practice

Practise union and literal types, narrowing, discriminated unions, interfaces, classes and type guards. Every solution has been type-checked in strict mode and run, and shows its real output.

Intermediate6 problemsUpdated

Review first: Union Types, Literal Types, Narrowing, Discriminated Unions, Interfaces, Type Guards and Assertion Functions

  1. 1.Narrow a union

    Beginner

    Write format(value: string | number | boolean): string that returns numbers with two decimals, booleans as "yes" or "no", and strings in upper case. Test it with all three types.

    Show hint

    Use typeof checks; each branch sees the narrowed type.

    Show solution
    TypeScript
    function format(value: string | number | boolean): string {
      if (typeof value === "number") return value.toFixed(2);
      if (typeof value === "boolean") return value ? "yes" : "no";
      return value.toUpperCase();
    }
    console.log(format(3), format(true), format("ok"));
    Output
    3.00 yes OK
  2. 2.Literal union options

    Beginner

    Define const SIZES = ["small", "medium", "large"] as const, derive a Size type from it, and write price(size: Size): number returning 120, 160 and 200. Print the prices for every size.

    Show hint

    type Size = (typeof SIZES)[number].

    Show solution
    TypeScript
    const SIZES = ["small", "medium", "large"] as const;
    type Size = (typeof SIZES)[number];
    
    function price(size: Size): number {
      switch (size) {
        case "small":
          return 120;
        case "medium":
          return 160;
        case "large":
          return 200;
      }
    }
    console.log(SIZES.map((s) => `${s}: ${price(s)}`).join(", "));
    Output
    small: 120, medium: 160, large: 200
  3. 3.A discriminated union

    Intermediate

    Model Circle (radius), Rectangle (width, height) and Square (side) as a discriminated union with a kind property. Write area(shape) with an exhaustive switch, and print the rounded areas of one of each.

    Show hint

    Add a default branch that assigns the shape to a never variable.

    Show solution
    TypeScript
    type Shape =
      | { kind: "circle"; radius: number }
      | { kind: "rectangle"; width: number; height: number }
      | { kind: "square"; side: number };
    
    function area(shape: Shape): number {
      switch (shape.kind) {
        case "circle":
          return Math.PI * shape.radius ** 2;
        case "rectangle":
          return shape.width * shape.height;
        case "square":
          return shape.side ** 2;
        default: {
          const unhandled: never = shape;
          return unhandled;
        }
      }
    }
    
    const shapes: Shape[] = [
      { kind: "circle", radius: 2 },
      { kind: "rectangle", width: 3, height: 4 },
      { kind: "square", side: 5 },
    ];
    console.log(shapes.map((s) => Math.round(area(s))));
    Output
    [ 13, 12, 25 ]
  4. 4.Extending interfaces

    Intermediate

    Create an interface Person (name, email) and an interface Student that extends it with rollNumber and marks: number[]. Write summary(s: Student): string returning "Asha (#7): average 88.3" style text.

    Show hint

    interface Student extends Person { ... }. Format the average with toFixed(1).

    Show solution
    TypeScript
    interface Person {
      name: string;
      email: string;
    }
    
    interface Student extends Person {
      rollNumber: number;
      marks: number[];
    }
    
    function summary(s: Student): string {
      const avg = s.marks.reduce((a, b) => a + b, 0) / s.marks.length;
      return `${s.name} (#${s.rollNumber}): average ${avg.toFixed(1)}`;
    }
    
    console.log(summary({ name: "Asha", email: "asha@example.com", rollNumber: 7, marks: [92, 85, 88] }));
    Output
    Asha (#7): average 88.3
  5. 5.A class with private state

    Intermediate

    Write a class Wallet with a private balance, add(amount: number), spend(amount: number): boolean (false if there is not enough money) and a balance getter. Show a successful and a failed spend.

    Show hint

    Use a #balance field (or private), and validate in spend.

    Show solution
    TypeScript
    class Wallet {
      #balance = 0;
    
      add(amount: number): void {
        this.#balance += amount;
      }
    
      spend(amount: number): boolean {
        if (amount > this.#balance) return false;
        this.#balance -= amount;
        return true;
      }
    
      get balance(): number {
        return this.#balance;
      }
    }
    
    const w = new Wallet();
    w.add(500);
    console.log(w.spend(200), w.spend(400), w.balance);
    Output
    true false 300
  6. 6.Validate unknown data with a type guard

    Intermediate

    Write a type guard isCourse(value: unknown): value is Course for interface Course { title: string; lessons: number }, and use it to keep only valid courses from a parsed JSON array that also contains invalid items.

    Show hint

    Check that the value is a non-null object and that each property has the right type.

    Show solution
    TypeScript
    interface Course {
      title: string;
      lessons: number;
    }
    
    function isCourse(value: unknown): value is Course {
      if (typeof value !== "object" || value === null) return false;
      const v = value as Record<string, unknown>;
      return typeof v.title === "string" && typeof v.lessons === "number";
    }
    
    const data: unknown[] = JSON.parse('[{"title":"HTML","lessons":46},{"title":"Bad"},null,{"title":"CSS","lessons":47}]');
    const courses = data.filter(isCourse);
    console.log(courses.map((c) => `${c.title} (${c.lessons})`));
    Output
    [ 'HTML (46)', 'CSS (47)' ]