Module 9 · TypeScript · Drills

Drills: TypeScript Foundations

Reading is not knowing. Type every one of these yourself — in the TypeScript Playground or a .ts file with strict on — before you reveal the solution. The compiler errors are the lesson.

How to use this page Each drill is a small task. Attempt it first, let tsc (or the Playground) check it, then click "Show solution" to compare. If yours type-checks differently but correctly — great, that's fluency. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 inference

Declare title, pages, and ready with values, letting TypeScript infer their types. Then write the type each one is inferred as in a comment. Do not annotate them.

Show solution
const title = "Intro.pdf";   // inferred: string
const pages = 12;            // inferred: number
const ready = true;          // inferred: boolean

Annotating these (: string, etc.) is redundant — let inference do the work and only annotate where it can't.

Drill 2 interface

Write an interface Document with id (string), title (string), pages (number), and an optional summary (string). Then create one object that satisfies it.

Show solution
interface Document {
  id: string;
  title: string;
  pages: number;
  summary?: string;   // optional: string | undefined
}

const doc: Document = {
  id: "d1",
  title: "Intro",
  pages: 12,
};   // valid — summary may be omitted

Drill 3 arrays + tuples

Type a variable tags as an array of strings, and match as a tuple of [string, number] (a filename and its similarity score). Assign valid values.

Show solution
const tags: string[] = ["ai", "uae"];
const match: [string, number] = ["intro.pdf", 0.87];

// tags.push(42)        // Error: number not assignable to string
// const m2: [string, number] = ["x"]  // Error: needs 2 elements

B · Stretch Intermediate

Drill 4 unions + narrowing

Write format(id: string | number): string. If it's a string, return it uppercased; if a number, return it as a fixed-0 string. Use typeof narrowing.

Show solution
function format(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();   // narrowed to string
  }
  return id.toFixed(0);          // narrowed to number
}

Without the typeof check, calling .toUpperCase() on a string | number is a compile error — narrowing is what unlocks the type-specific methods.

Drill 5 discriminated union

Model an ask result as a union: { status: "ok"; answer: string } or { status: "error"; message: string }. Write render that switches on status and returns the right field.

Show solution
type AskResult =
  | { status: "ok"; answer: string }
  | { status: "error"; message: string };

function render(r: AskResult): string {
  switch (r.status) {
    case "ok":
      return r.answer;    // .message not available here
    case "error":
      return r.message;   // .answer not available here
  }
}

The shared literal field status is the discriminant. Switching on it narrows the type in each branch automatically.

Drill 6 generics

Write a generic first<T>(items: T[]): T | undefined that returns the first element. Call it with strings and with numbers and note the inferred return types.

Show solution
function first<T>(items: T[]): T | undefined {
  return items[0];
}

const a = first(["x", "y"]);  // a: string | undefined
const b = first([1, 2]);      // b: number | undefined

The | undefined is honest: an empty array has no first element. With noUncheckedIndexedAccess on, TypeScript insists you account for it.

Drill 7 utility types

Given interface Document { id: string; title: string; pages: number; }, create three derived types: a patch type (all optional), a create type (no id), and a card type (only id and title).

Show solution
type DocPatch  = Partial<Document>;            // all fields optional
type NewDoc    = Omit<Document, "id">;          // id assigned by the DB
type DocCard   = Pick<Document, "id" | "title">;  // just these two

Drill 8 typing an API response

Write an async function getDocuments that fetches /api/documents and returns Promise<Document[]>. Close the any hole that res.json() leaves.

Show solution
async function getDocuments(): Promise<Document[]> {
  const res = await fetch("/api/documents");
  const data = (await res.json()) as Document[];  // assert at the edge
  return data;
}

res.json() is typed any by design. Asserting the shape once, at the boundary, keeps the rest of your code fully typed.

C · Build challenge Build

Mini-project Write types.ts: model DocChat's full API type layer — the shared contract between FastAPI and Next.js. Include the domain models, a generic ApiResponse<T> envelope, and the request/response pair for the ask endpoint. This is the literal file you'll commit on day one of the project.

Build · DocChat API types

Define: Document (with a status union), a Role literal union, ChatMessage, a generic ApiResponse<T> (ok/error discriminated union), AskRequest, Source, and AskResponse.

Show solution
// ---- Domain models ----
interface Document {
  readonly id: string;
  title: string;
  pages: number;
  status: "pending" | "ready" | "failed";
  uploadedAt: string;   // ISO timestamp
}

type Role = "user" | "assistant";

interface ChatMessage {
  readonly id: string;
  role: Role;
  text: string;
  createdAt: string;
}

// ---- Generic API envelope ----
type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

// ---- Ask endpoint ----
interface AskRequest {
  documentId: string;
  question: string;
  topK?: number;
}

interface Source {
  documentId: string;
  snippet: string;
  score: number;   // pgvector similarity
}

interface AskResponse {
  answer: string;
  sources: Source[];
}

Bonus: write the ask(req: AskRequest): Promise<ApiResponse<AskResponse>> helper and prove that narrowing on result.ok gives you result.data in one branch and result.error in the other — with no casting.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

What does strict mode's headline check do?
Strict null checks — string can't secretly be null/undefined; you must handle the missing case.
click to flip
any vs unknown?
any turns checking off (unsafe). unknown forces you to narrow before use (safe) — use it for raw JSON.
click to flip
Make every field of T optional?
Partial<T>. Drop a key with Omit<T, "k">; keep some with Pick<T, "k">.
click to flip
interface vs type — when must you use type?
For unions, intersections, tuples, or aliasing a primitive — things interface can't express.
click to flip
Enum or union of literals for a fixed set?
Prefer a union of string literals — simpler, erases to plain strings, plays nicely with JSON.
click to flip
What is structural typing?
A value fits a type if it has the right shape, regardless of its declared name — "duck typing, checked".
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? You can now describe data so the compiler protects you. Next we put these types to work in the UI: Lesson 9.2 — React 19 & Next.js 15, where DocChat's frontend comes alive.