Skip to content

Hands‑On Exercise: Build a Safe Parser for External API Data

We’ll simulate an API that returns a list of products.

The API is unreliable—it may return malformed data, missing fields, or wrong types.

Your job is to:

  1. Define the expected TypeScript types
  2. Write custom type guards
  3. Validate the external data
  4. Narrow types safely
  5. Use exhaustiveness checking to ensure all cases are handled

Let’s go step by step.


Step 1 — Define the Expected Data Shape

Create a file:

types.ts

export type Product = {
  id: number
  title: string
  price: number
  tags: string[]
}

This is the ideal shape we want to enforce.


Step 2 — Simulate External API Data

Create:

api.ts

export async function fetchProducts(): Promise<unknown> {
  // Simulated external API response
  return JSON.parse(`
    [
      { "id": 1, "title": "Laptop", "price": 1299, "tags": ["tech"] },
      { "id": "oops", "title": "Phone", "price": 799, "tags": ["tech"] }
    ]
  `)
}

Notice the second product has an invalid id (string instead of number).

TypeScript cannot catch this at compile time—this is runtime data.

We must validate it.


Step 3 — Write Type Guards

Create:

guards.ts

Start with a guard for a single product.

import { Product } from "./types"

export function isProduct(value: unknown): value is Product {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as any).id === "number" &&
    typeof (value as any).title === "string" &&
    typeof (value as any).price === "number" &&
    Array.isArray((value as any).tags) &&
    (value as any).tags.every((t: unknown) => typeof t === "string")
  )
}

Now a guard for an array of products:

export function isProductArray(value: unknown): value is Product[] {
  return Array.isArray(value) && value.every(isProduct)
}

This is a composable guard—a best practice in TypeScript.


Step 4 — Parse and Validate the Data

Create:

parser.ts

import { fetchProducts } from "./api"
import { isProductArray } from "./guards"
import { Product } from "./types"

export async function loadProducts(): Promise<Product[]> {
  const data: unknown = await fetchProducts()

  if (!isProductArray(data)) {
    throw new Error("Invalid product data received from API")
  }

  return data
}

TypeScript now guarantees:

  • loadProducts() returns Product[]
  • Every product has correct types
  • No unsafe property access

Step 5 — Use the Parser in Your App

Create:

index.ts

import { loadProducts } from "./parser"

async function main() {
  try {
    const products = await loadProducts()

    for (const p of products) {
      console.log(`${p.title} — $${p.price}`)
    }
  } catch (err) {
    console.error("Failed to load products:", err)
  }
}

main()

Run it:

ts-node index.ts

You should see an error because the second product is invalid.

This is exactly what we want.


Step 6 — Add Exhaustiveness Checking (Bonus)

Let’s extend the API to return:

  • success
  • error
  • loading

api.ts

export type ApiResponse =
  | { status: "success"; data: unknown }
  | { status: "error"; message: string }
  | { status: "loading" }

Handle all cases safely

import { ApiResponse } from "./api"
import { isProductArray } from "./guards"

function handleResponse(res: ApiResponse) {
  switch (res.status) {
    case "success":
      if (!isProductArray(res.data)) {
        throw new Error("Invalid product data")
      }
      return res.data

    case "error":
      throw new Error(res.message)

    case "loading":
      console.log("Loading...")
      return []

    default:
      const _exhaustive: never = res
      return _exhaustive
  }
}

If someone adds a new variant:

{ status: "empty" }

TypeScript will immediately flag missing cases.

This is industrial‑strength safety.


What You Learned

This hands‑on exercise demonstrates how to safely handle external data in TypeScript using:

1. Custom type guards

  • value is Type functions
  • Composable guards
  • Array guards

2. Narrowing

  • Using guards to refine unknown
  • Safe property access
  • Eliminating runtime errors

3. Exhaustiveness checking

  • never in switch statements
  • Catching missing union cases
  • Ensuring future‑proof logic

4. Real‑world application

  • Parsing API responses
  • Validating JSON
  • Handling unreliable external data

This is exactly how professional TypeScript teams build robust, production‑ready systems.