Types as Guardrails for AI Code
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
AI can write TypeScript faster than you can manually inspect every branch. That is exactly why the compiler, typed lint, and strict domain types matter more when AI is on the team.
The killer combo is simple: AI drafts, the compiler referees. You still review the code, but you do not rely on vibes. You run the same TypeScript program checks you would require from a teammate.
Let the compiler reject invented APIs
A hallucination is a confident answer that sounds real but is false. In TypeScript, a hallucination often looks like a property, method, option, or package API that does not exist.
If your types are strict, many of those guesses die at compile time:
ticket.ticketIdfails when the type says the property isid.response.payload.userfails when the union says the success member usesdata."archived"fails when the state union is"draft" | "published".config.retryCountfails when the config object has no such key.
That does not make generated code correct. It means the assistant must satisfy the contracts your project already wrote down.
T-100 open:Nina !
T-101 closed:unassigned -
The commented draft is the kind of thing an assistant might invent from nearby vocabulary: ticketId, state, and urgent all sound plausible. They are not in the type. When you uncomment the block, TypeScript rejects those properties before the code can run.
That is a guardrail, not a guarantee. If the assistant writes the wrong formula using real properties, the compiler may accept it. The guardrail catches shape mismatches first so your human review can focus on behavior.
Put AI changes through the same pipeline
Do not review generated TypeScript by reading it in isolation. Review it with the project machinery from Sections 16 through 19.
For a local AI-assisted change, the minimum loop is usually:
npm run typecheck
npm run lint
npm run test:types
npm run test:runIn many projects, typecheck is tsc --noEmit. That command is valuable because it asks the TypeScript checker to prove the project is coherent without writing JavaScript. Typed lint adds policy the compiler does not own, such as rejecting floating promises, unnecessary conditions, unsafe member access, or ignored promises when your team enables those rules.
Your review pipeline should answer different questions:
tsc --noEmit: do the source files satisfy the type contracts?- Typed lint: does the code violate team safety policy?
- Type tests: did public inference or exported API behavior change?
- Runtime tests: does the code do the right thing for real inputs?
- Build: can the app or package still be produced?
If an assistant suggests "this should compile," run the command. If it suggests "this preserves behavior," run or write the test. The assistant's claim is not evidence until your tools agree.
Design types that leave fewer escape routes
AI works from patterns. If your project types are vague, generated code has more room to be vague too.
Weak types invite weak generated code:
type Task = {
id: string;
status: string;
payload: Record<string, unknown>;
};That shape might be honest for a raw boundary value, but it is not a good internal domain type. The assistant can write if (task.status === "done") even if your product only knows "queued" | "running" | "failed" | "succeeded". It can pull arbitrary fields from payload and make the reviewer chase runtime assumptions.
A stricter internal type makes the draft harder to fake:
type TaskStatus = "queued" | "running" | "failed" | "succeeded";
type Task =
| { status: "queued"; id: string }
| { status: "running"; id: string; startedAt: string }
| { status: "failed"; id: string; message: string }
| { status: "succeeded"; id: string; resultId: string };Now a generated switch has to deal with the real cases. If the assistant invents "done", the compiler complains. If it forgets "failed" and you use the exhaustiveness habits from Section 15, the missing branch shows up during typecheck.
This is not "make types clever because AI exists." It is "make the important states explicit because humans and AI both review better when the contract is visible."
Prompt for compiler-friendly drafts
When you ask AI for TypeScript code, make the guardrails part of the prompt:
Task: Implement this function using the existing types.
Constraints:
- do not change public types
- do not use loose types, assertions, or ignore comments
- handle every union member explicitly
- return Result<T, E> instead of throwing for expected failures
Checks I will run:
- npm run typecheck
- npm run lint
- npm run test:types
- npm run test:run
Output:
- code first
- then list any assumptions that the compiler cannot proveThe last line matters. The compiler can reject invented properties. It cannot know your pricing rule, permission policy, or whether a message is misleading. Ask the assistant to separate "the compiler can check this" from "a human must verify this."
What still gets past the referee
Types catch shape errors, missing cases, unsafe calls, and many stale assumptions. They do not catch every kind of wrongness.
Generated code can still:
- Use the right property with the wrong formula.
- Choose the wrong default value.
- Make an allowed network call at the wrong time.
- Preserve a stale business rule.
- Pass a type by hiding evidence behind an assertion or loose value.
- Add a dependency or command your team never agreed to run.
That is why the review loop has layers. The compiler referees static contracts. Typed lint enforces team policy. Tests prove examples of runtime behavior. Human review checks purpose, security, privacy, maintainability, and whether the code is a change you can explain.
The next lesson focuses on the failures that still compile: plausible-but-wrong logic, generated escape hatches, and the responsibility line when your name is on the commit.
Checkpoint
Answer all three to mark this lesson complete