Why TypeScript Makes AI-Generated Code Safer
Types turn a whole class of AI mistakes — invented properties, wrong arguments, forgotten null checks — into errors caught before the code runs. How TypeScript acts as a feedback loop for agents, the settings that matter, and the escape hatches AI uses to switch it off.
AI coding agents write code faster than anyone can review it line by line. So the question becomes: what checks the code mechanically, every time, before a human looks? Tests are one answer. The other, cheaper and broader, is a type checker — and for JavaScript projects, that means TypeScript.
This isn't a style preference. It changes how well agents perform.
The mistakes types catch
A large share of AI coding errors are the kind a type checker is designed to catch:
Invented properties. The model "remembers" that the user object has a fullName field. It doesn't — it's name. In JavaScript, user.fullName is silently undefined, and the bug surfaces later as a blank label. In TypeScript, it's a compile error pointing at the exact line.
Wrong function signatures. A call passes arguments in the wrong order, or passes a string where an object was expected, or uses an options key a library removed two versions ago.
Forgotten null cases. const user = await db.user.findUnique(...) can return null. The code immediately reads user.email. With strict null checks on, TypeScript refuses until the null case is handled.
Hallucinated APIs. A method that doesn't exist on a library's client. With the library's type definitions installed, calling it fails to compile.
Shape drift across files. The agent changes a function's return type in one file and doesn't update three callers. TypeScript lists all three.
Each of these, in plain JavaScript, is a runtime bug discovered by a user. In TypeScript, it's a red line discovered in seconds.
Types as an agent feedback loop
What makes coding agents effective is the loop: act, observe, adjust. (What Is an AI Coding Agent?) The quality of the loop depends on the quality of the signal.
tsc --noEmit is an excellent signal:
- Fast — seconds, usually, versus minutes for a test suite.
- Broad — it checks every file, including paths no test exercises.
- Precise — file, line, and a description of the mismatch.
- Deterministic — no flaky failures.
An agent that runs the type checker after each change catches its own mistakes before you ever see them. Make it automatic:
- Put
npm run typecheckin yourCLAUDE.mdas something to run after changes. (How to Write a CLAUDE.md.) - Or enforce it with a
Stophook, so Claude Code can't finish a turn with type errors. (Claude Code Hooks.) - Run it in CI on every pull request. (Set Up CI With GitHub Actions.)
Types also help the model before it writes anything: type definitions are compact, precise documentation of what exists. An agent reading types.ts learns your data model faster than one inferring it from scattered usage.
The compiler settings that matter
TypeScript's protection depends heavily on configuration. In tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
strict: trueis the one that matters most. It enables strict null checks, no implicitany, and several others. A non-strict TypeScript project catches far less.noUncheckedIndexedAccessmakesarray[i]andrecord[key]possiblyundefined, which is true at runtime and a common source of crashes.
If you're converting an existing project and strict produces hundreds of errors, turn it on anyway and let an agent work through them in batches. It's exactly the kind of mechanical, verifiable task agents do well.
The escape hatches agents reach for
Here's the catch. When a type error is inconvenient, an AI will often make it go away rather than fix it. Watch for these in diffs:
any
const data: any = await res.json()
any switches type checking off for that value — and everything derived from it. One any at an API boundary can silently untype half a feature.
Non-null assertions (!)
const email = user!.email
"Trust me, it's not null." It's the null check, deleted.
Type assertions (as)
const order = body as Order
Tells the compiler to believe the value has a shape, without checking. Particularly dangerous on data from outside: request bodies, API responses, database JSON.
@ts-ignore / @ts-expect-error
Suppresses the error on the next line entirely.
Each has legitimate uses, but in AI-written code they're usually a sign the agent resolved a type error by removing the check rather than fixing the bug. Counter it:
- Add a rule to
CLAUDE.md: "Never useany, non-null assertions, or@ts-ignoreto fix a type error. If a type is wrong, fix the type or handle the case." - Enable ESLint rules that flag them:
@typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,@typescript-eslint/ban-ts-comment. - Search every AI-written PR for
any,as,!., andts-ignore. (How to Review a Pull Request Written by an AI Agent.)
Types stop at the boundary
TypeScript checks your code. It can't check data arriving at runtime — a request body, an API response, a webhook payload, a JSON column. If an external API returns something different from what your types claim, TypeScript won't know.
So validate at the boundaries with a runtime schema library — Zod, Valibot, ArkType, and similar — and derive the TypeScript types from the schema:
import { z } from 'zod'
const CreateOrder = z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive().max(100),
})
type CreateOrder = z.infer<typeof CreateOrder>
const input = CreateOrder.parse(await req.json()) // throws on bad input
One definition gives you runtime validation and compile-time types, and it's where as SomeType should be replaced. It's also a security control: validated input is the first line of defence against malformed and malicious requests.
Generated types from your database and APIs
The best types aren't hand-written — they're generated from the source of truth:
- ORMs like Prisma and Drizzle produce types from your schema.
- Supabase and others can generate types from the live database.
- OpenAPI specs can generate typed API clients.
When the schema changes, regenerate, and the compiler immediately lists every place in the code that needs updating — a to-do list an agent can work through.
What types don't do
Types verify consistency, not correctness. Code that computes the wrong discount type-checks fine. Code that shows one user another user's data type-checks fine. You still need tests for behaviour and review for logic and security. (Getting AI to Write Tests That Actually Catch Bugs.)
But types remove an entire category of errors cheaply, and give agents a fast, honest signal on every change. For code written at AI speed, that's a very good trade.
The checklist
- TypeScript with
strict: true(plusnoUncheckedIndexedAccess) - Type check in
CLAUDE.md, a Stop hook, and CI - Lint rules against
any, non-null assertions, andts-ignore - Runtime validation (Zod or similar) at every external boundary
- Types generated from the database schema and API specs
- AI PRs reviewed for new escape hatches
EasySpawn runs Claude Code in a workspace with your full toolchain installed, so it can run the type checker, linter, and tests after every change — and fix what they find before you review. See how it works or join the waitlist.
Related: What Is a Tech Stack? · Claude Code Hooks: Rules the Agent Can't Forget · Validating Input With Zod · Linters and Formatters Explained
Keep reading
Getting AI to Write Tests That Actually Catch Bugs
Ask an AI for tests and you'll get plenty: tests that mock everything, assert nothing useful, and pass no matter what the code does. How to get tests that fail when behaviour breaks — what to test, how to prompt, how to check a test is real, and how tests become the agent's safety net.
How to Undo Almost Anything in Git (Including an AI Agent's Mess)
An agent committed to the wrong branch, rewrote files you needed, or ran a reset it shouldn't have. Git can almost always get your work back. Which undo command fits which situation — restore, revert, reset, and the reflog that rescues 'deleted' commits — explained with the exact commands.