Module 9 · TypeScript · Drills
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.
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.
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.
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.
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.
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
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.
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.
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.
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).
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.
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.
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.
// ---- 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.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.
strict mode's headline check do?string can't secretly be null/undefined; you must handle the missing case.any vs unknown?any turns checking off (unsafe). unknown forces you to narrow before use (safe) — use it for raw JSON.T optional?Partial<T>. Drop a key with Omit<T, "k">; keep some with Pick<T, "k">.interface vs type — when must you use type?interface can't express.Tick each only if you can do it without looking:
ApiResponse<T> envelope