Types as a check on the AI's work
five errors caught before the run, and one that gets through
What it is
The chapter's central article. Go back to chapter 4: ten articles there on catching by hand the errors you can't see when the code runs. Types do part of that work mechanically, at every check, for free.
Below are five real AI mistakes and the compiler's real messages for them. And then an honest limitation: one error of the same kind slips past, and knowing about it matters more than the other five.
What it looks like in code
1. Forgot the emptiness check
The most frequent AI mistake in the whole book:
function report(c: City): string {
return `${c.name}: ${c.temp.toFixed(1)}°`;
}error TS18047: 'c.temp' is possibly 'null'.2. Got a field name wrong
const c: City = { name: "Oslo", temperature: 4 };error TS2353: Object literal may only specify known properties,
and 'temperature' does not exist in type 'City'.In JavaScript without types this would quietly have created an object with
one extra field and without the one needed. The error would have surfaced
somewhere else and looked like undefined.
3. Got the argument order wrong
function subscribe(userId: number, city: string): void {}
subscribe("Oslo", 7);error TS2345: Argument of type 'string' is not assignable
to parameter of type 'number'.A classic when working with an AI: it remembers a function with a similar name and a different argument order.
4. Forgot a variant of a union
type Status = "paid" | "pending" | "refunded";
function label(s: Status): string {
switch (s) {
case "paid": return "paid";
case "pending": return "awaiting";
}
}error TS2366: Function lacks ending return statement
and return type does not include 'undefined'.refunded is missing. Without types the function would silently have
returned undefined, and an empty space would have appeared in the
interface instead of a label.
This is the important case: add a fourth status tomorrow and the compiler will find every place that doesn't handle it. You don't find that by hand.
5. Indexing out of bounds
const cities = ["Oslo", "Bergen"];
const first: string = cities[5];error TS2322: Type 'string | undefined' is not assignable to type 'string'.Only works with noUncheckedIndexedAccess (article 9.3).
Where types don't help
Now, honestly. Here is code the compiler lets through without a single error:
interface WeatherResponse { main: { temp: number }; name: string }
async function fetchWeather(city: string): Promise<WeatherResponse> {
const raw = '{"cod":"404","message":"city not found"}';
return JSON.parse(raw) as WeatherResponse;
}
const data = await fetchWeather("Tonsberg");
console.log(data.main.temp);tsc: no errorsfailed: Cannot read properties of undefined (reading 'temp')as WeatherResponse is a promise to the compiler, not a check. You said
"trust me, this has that shape", and it did. Something else arrived — and
there is the very same Cannot read properties of undefined that chapter 8
opened with.
The boundary is everything that arrived from outside the program:
- an API response (
fetch,JSON.parse); - form data;
localStorage;- parameters from the page's address.
What closes the boundary
Run-time validation libraries — zod, valibot and the like. They do for
the frontend what pydantic does for the backend (article 7.3): you
describe the shape once, and it both checks the data and gives you a type.
The API response is currently cast with `as`. Replace it with
a run-time check using zod: schema, parse, a clear error if
the shape doesn't match. Infer the type from the schema, don't
duplicate it by hand.That last sentence matters: the point is for the shape to be described once and serve as both the check and the type. Two descriptions side by side will drift apart.
What to tell the AI
Types are also a way of stating the task. A signature is more precise than words (articles 3.4, 6.5):
Write a function to this signature:
async function loadSubs(userId: number): Promise<Result<City[]>>
where Result<T> = { ok: true; data: T } | { ok: false; error: string }.
Don't throw exceptions outward, return errors as the second
variant. tsc --noEmit with strict must pass.Checking finished code:
Run tsc --strict over your own code in your head: what errors
would it find? Fix them without using any, as or ts-ignore.The prohibition in the last line is mandatory — otherwise the "fix" will be a gag (article 9.6).
Cheat sheet
Types catch before the run: a forgotten emptiness, a wrong field, argument order, a missed union variant, going past the end of an array.
Add a variant to a union and the compiler finds every unhandled place.
asis a promise to the compiler, not a check. It doesn't look at outside data.The boundary: API responses, forms,
localStorage, the page's address.The boundary is closed by a run-time check,
zodfor example.Infer the type from the schema instead of duplicating it alongside.