Settings
Your save file

Progress lives in this browser, on this device. Download the save file to move it to another device, or to keep a copy in case this browser clears its storage.

Season 2
Week 11 of 12

Week 11 · Endgame

Final HOH

The typed fetch wrapper

getJson<T>: the generics payoff. Plus declaration files and @types.

This is the function that makes the whole season pay rent. Every stat page, every eviction tracker, every houseguest profile on BBJ starts with the same three lines: call fetch, await the response, parse the body. Right now those three lines get copy-pasted into a dozen files, each one trusting the shape of what comes back a little differently. This week you write that pattern once, generic over whatever shape each caller actually needs, and every one of those call sites gets to delete its own copy.

Building the wrapper, line by line

Start from the plain JS version you have written a hundred times. Nothing about it is wrong, exactly, it just has no memory of what shape the response is supposed to be, and it throws away the status code the moment something goes sideways.

The ES6 you write

async function loadJson(url, fetcher) {
  const res = await fetcher(url)
  const data = await res.json()
  return data
}

The TS version

type Fetcher = (url: string) => Promise<{
  ok: boolean
  status: number
  json(): Promise<unknown>
}>
 
async function fetchTyped<T>(url: string, fetcher: Fetcher): Promise<T> {
  const res = await fetcher(url)
  if (!res.ok) {
    throw new Error('Request to ' + url + ' failed with status ' + res.status)
  }
  // Boundary cast: json() only ever promises unknown, T is the caller's claim.
  return (await res.json()) as T
}

What changed

fetchTyped adds a generic T and a Fetcher type for the injected fetch function, a Promise<T> return type, and a real Error carrying the status code instead of swallowing a failed response silently. The await-based flow does not change at all, same two awaits in the same order. What changed is that the function now makes a claim, in its own signature, about what it hands back, and it stops pretending a failed request is nothing to talk about.

Read it top to bottom. <T> right after the function name declares the type parameter, same spot it lived in back in week six's generics. Promise<T> as the return type is the whole contract: whatever the caller says T is, that is what this function promises to eventually hand back. await fetcher(url) runs the injected fetch, exactly like calling fetch directly, except the sandbox (and your tests) can hand it a fake that never touches a real network. if (!res.ok) catches a failed request before anything downstream has to guess why the data looks wrong, and throws a real Error with the status code baked into the message instead of a silent undefined three components later. Only after all of that does the function touch the body at all.

The honest lie: where the cast is fine, and where week 8's guards earn their keep

res.json() returns Promise<unknown>, always, because a JSON parser genuinely cannot know your shape, the same fact week eight opened with. as T on the last line does not prove anything, it tells TypeScript "trust me, this is a T," which is exactly why it gets a comment: an uncommented cast anywhere on this site breaks the house rule the same way an uncommented any does, it is just as capable of lying quietly. For data you are about to render straight into the page, that trust is usually fine, a malformed response will surface fast as a broken UI. For data you are about to trust with something that matters, the exact WordPress payload shape from week eight is the better example, pair the cast with a real guard like isWpComment instead of trusting the boundary alone. The generic wrapper stays generic either way; the guard is a decision you make at each call site, not something fetchTyped itself can enforce for you.

Declaration files and @types packages

Every npm package that ships with TypeScript support either bundles its own .d.ts files (a declaration file: type information with no runtime code at all, describing what a .js file exports without containing any of the logic itself) or relies on the community-maintained @types scope on npm. npm i -D @types/some-package installs exactly that: a separate package containing nothing but declaration files, written by someone other than the original author, describing that package's shape well enough for TypeScript to check your usage of it. Some packages need no @types install at all, because they wrote and shipped their own .d.ts files in the same package you already installed. Whether you need the separate @types package or not, none of it changes what runs. Declaration files exist purely for the compiler and your editor, the same erased-at-runtime deal every type in this course has been since week one.

That is the whole lesson. Now prove it in the comp below.

Up next

Week 12 · Finale Night

Strict mode and the migration playbook

Locked until you win