The WordPress REST API is a stranger you let into your data layer every single time a page loads. It has never seen your TypeScript types, it never will, and it will change shape the moment somebody adds a plugin or a custom field without telling you. Everything up to this point in the course has been about types you write yourself, on data you control. This week is about the boundary where that stops being true, which for bigbrotherjunkies.com is every single fetch call.
any vs unknown vs never: trust nobody, verify, impossible
Three types, three very different promises, and mixing them up is where a lot of TypeScript code quietly stops protecting anyone.
anyturns off type checking for that value entirely. Read a property, call it, pass it anywhere, TypeScript stays silent the whole time. Trust everyone, verify nothing.unknownis the safe version of the same idea. Anything can flow into anunknown, same asany, but you cannot do a single thing with it, no property access, no method call, until you have proven what it actually is with a real check. Trust nobody, verify first.neveris the type with no valid values at all, the type of a branch that is provably unreachable. You already met this in week three's exhaustiveness check. Impossible, not untrusted, a genuine promise that execution never reaches here.
The move this week leans on unknown specifically: it is the type that forces you to write the
verification instead of skipping it.
Typing the boundary instead of trusting it
res.json() has always returned Promise<any>. That is not a bug, it is the honest admission
that a JSON parser cannot know your data's shape, only that it parsed successfully. Left alone,
that any quietly spreads through every function that touches the result. Stop it at the door.
The ES6 you write
async function loadComment(id) {
const res = await fetch(`/wp-json/wp/v2/comments/${id}`)
const data = await res.json()
return data.content.rendered
}The TS version
interface WpComment {
id: number
author_name: string
content: { rendered: string }
}
function isWpComment(value: unknown): value is WpComment {
if (typeof value !== 'object' || value === null) return false
if (!('content' in value)) return false
if (typeof value.content !== 'object' || value.content === null) return false
if (!('rendered' in value.content)) return false
return typeof value.content.rendered === 'string'
}
async function loadComment(id: number): Promise<string> {
const res = await fetch(`/wp-json/wp/v2/comments/${id}`)
const data: unknown = await res.json()
if (!isWpComment(data)) throw new Error('The feeds are lying to you')
return data.content.rendered
}What changed
data goes from an implicit any, courtesy of res.json()'s own return type, to an explicit unknown, and a guard sits between it and the return statement. Nothing about the network call changed. What changed is that TypeScript now refuses to let loadComment read data.content.rendered until isWpComment has actually proven the shape at runtime, not just promised it in a type annotation.
Notice the guard is nothing but narrowing from week five, aimed at the network instead of at a
local union: typeof for primitives, in to prove a property exists before reading it, one
if-return-false per field. Nothing new syntactically, just pointed at data you did not create.
The 'content' in value check is doing real work, not decoration. value is typed unknown
right up until that line. Every in check that follows narrows it a little further, the same
control-flow narrowing from week five's guard, just chained field by field until value earns its
way up to the full WpComment shape.
That is the whole lesson. Now prove it in the comp below.