The trust boundary
Types describe values you wrote. Anything from outside your code is a stranger until you check.
You wrote a function that takes a User and shows their name. Locally, every test passes. In production, the page goes blank because user.name is undefined — the API returned a record without a name field that day, and TypeScript never noticed. The compiler isn't broken. It just trusted you when you said the data was a User, and you were wrong.
This lesson is about that quiet promise — the moment where TypeScript stops checking and starts hoping.
Where types are honest
Most of your code is data that you wrote yourself. You called const user = { name: "Ada", age: 31 } two lines up. TypeScript can read those two lines, see the shape, and know exactly what you have. The type is a fact about a value the compiler can see.
const user = { name: "Ada", age: 31 };
function greet(u: { name: string }) {
return "hi " + u.name;
}
greet(user); /* TS sees both — checks the call */The compiler sees the literal on the first line. It sees the parameter shape on the third. It compares them and tells you yes or no. There is no gap between the type and the value, because the value never left your file.
This is the world where types are honest. Function arguments, return values, local variables, anything constructed from those — TypeScript can see the whole graph and check every edge.
Where types start lying
Now consider data that comes from outside your bundle. A fetch response. A message from a websocket. A row from a database client. A string you read from localStorage. Each one was written somewhere TypeScript cannot see — by your server, by yesterday's version of your app, by another team, by a user with the dev tools open.
type User = { name: string; age: number };
const res = await fetch("/api/me");
const user = (await res.json()) as User; /* a hopeful lie */
console.log(user.name.toUpperCase());Read the cast on line four carefully. You told the compiler "this is a User." TypeScript believed you. It did not look. It cannot look — the JSON came over the wire as a string, was parsed at runtime, and exists only as bytes the compiler never saw.
If the server returned { "username": "Ada", "age": 31 }, your code crashes on toUpperCase, because user.name is undefined. The error isn't a TypeScript error. It's a runtime error in a file the compiler said was fine.
An as cast on data from outside your code is a promise you cannot keep. The compiler stops checking; the runtime does not start.
const data = JSON.parse(text) as Order where text came from the network. What did this as actually do?Unknown is the polite answer
Anything from outside your bundle should land in your code as unknown — TypeScript's word for "I have no idea what this is." It's the polite version of any. With any, you can do anything to the value and the compiler stays quiet. With unknown, you can hold the value, pass it around, but you cannot use it until you've proven what it is.
async function loadUser(): Promise<unknown> {
const res = await fetch("/api/me");
return res.json(); /* res.json() is Promise<any> — we narrow it */
}
const data = await loadUser();
data.name; /* error: 'data' is of type 'unknown' */The last line refuses to compile. TypeScript is not being annoying — it's telling you the truth. You haven't checked anything yet. Reading data.name is a guess, and the compiler won't sign off on guesses.
This is the move that makes the rest of the course possible. By putting unknown at the boundary instead of User, you turn an invisible runtime bug into a visible compile-time question: "how do you know this is a User?" The next lessons answer that question with code that runs.
any turns the type system off. unknown turns the type system on and asks you to do the work. Prefer it at every boundary.
Drawing the boundary
A useful image: imagine a wall through your codebase. On the inside, every value has a real type the compiler can see. On the outside, everything is unknown. The wall has gates — the places where outside data comes in. Your job is to put a checker at every gate.
The gate is one of three things, and the rest of the course is mostly about each one:
A type guard — a function that returns a boolean and teaches the compiler what the value is when the boolean is true. Lesson 2.
An assertion function — a function that throws when the value is wrong, and otherwise narrows the type for the rest of the block. Lesson 3.
A schema — a description of the expected shape that doubles as a runtime validator and a TypeScript type. Lessons 4 through 7.
Each of these is a way to do the same job: cross the boundary with proof, not a promise.
localStorage, parse it with JSON.parse, and TypeScript types the result as any. Why is that bad?The plan from here
Every lesson in this course is a different way to put a check at a gate. Lesson 2 starts with the smallest tool — a function that returns true or false and tells the compiler what you've learned. By the end, you'll have a default move for every place outside data enters your app. The boundary will still exist; it just won't be invisible anymore.
const x = JSON.parse(raw) as Settings in a code review. What is the most accurate critique?