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.