Why TypeScript Needs AI Rules Despite Having Types
TypeScript's type system should make AI-generated code safer by default. And it does — compared to plain JavaScript, TypeScript output from AI assistants is significantly better. But 'better than JavaScript' isn't the bar. The bar is 'matches your team's TypeScript standards,' and that's where AI assistants consistently fall short.
The most common issues: using `any` as an escape hatch (the AI does this when it can't figure out the right type), generating overly broad types instead of specific discriminated unions, skipping Zod validation at API boundaries (trusting req.body without parsing), ignoring your framework's type conventions (Next.js, tRPC, Drizzle each have specific type patterns), and generating `as` type assertions instead of type guards.
These aren't obscure issues — they're the ones that show up in every TypeScript team's code reviews. A handful of rules eliminates them.
Rule 1: No `any`, No Type Assertions
`any` defeats the purpose of TypeScript. When AI assistants encounter a type they can't figure out, they reach for `any` — and suddenly your type-safe code has a hole. Type assertions (`value as SomeType`) are the polite version of the same problem: they tell the compiler 'trust me' without any runtime verification.
The rule: 'Never use `any` type. Use `unknown` when the type is genuinely unknown, then narrow it with type guards. Never use type assertions (`as`). Use type guard functions (isFoo(x)) or Zod parsing to narrow types safely. If the correct type is hard to express, create a named type alias rather than using `any`.'
This rule forces the AI to think about types properly — and it usually can. Most `any` usage is laziness, not necessity. When the AI is forced to find the right type, it generates better code.
Most AI `any` usage is laziness, not necessity. When forced to find the right type, the AI usually can. The rule 'Never use any — use unknown + type guards' produces dramatically better code.
Rule 2: Zod Validation at Every Boundary
TypeScript types exist at compile time only — they're erased at runtime. This means req.body, API responses, localStorage values, and URL parameters are all `unknown` at runtime, regardless of what type you assign them. Without runtime validation, a malformed request crashes your app instead of returning a 400.
The rule: 'Use Zod schemas to validate all external input: API request bodies, query parameters, URL params, form data, and data from external APIs. Parse before processing — never access properties on unvalidated data. Infer TypeScript types from Zod schemas using z.infer<typeof schema> to keep types and validation in sync.'
The second sentence is critical: 'Infer types from schemas.' Many teams define both a TypeScript interface and a Zod schema for the same data shape, which creates drift. Inferring the type from the schema guarantees they match.
Infer TypeScript types from Zod schemas with z.infer<typeof schema>. This guarantees your types and runtime validation always match — no drift, no separate definitions to maintain.
Rule 3: Discriminated Unions Over Broad Types
AI assistants generate broad types with optional fields when they should generate discriminated unions. Instead of `{ status: string; data?: any; error?: string }`, the correct type is `{ status: 'success'; data: User } | { status: 'error'; error: string }`. The discriminated union makes impossible states unrepresentable — you can't have both data and error, or neither.
The rule: 'Use discriminated unions for types with variants. Include a literal discriminant field (status, type, kind) that narrows the type. Never use optional fields to represent different states of the same data. Make impossible states unrepresentable at the type level.'
This rule has a compounding effect: once the AI generates discriminated unions, switch statements and type narrowing work automatically. The code becomes both type-safe and self-documenting — the type tells you exactly what states are possible.
Once the AI generates discriminated unions, switch statements and type narrowing work automatically. The code becomes both type-safe and self-documenting.
Rule 4: Framework-Specific Type Patterns
Every TypeScript framework has its own type patterns that AI assistants need to learn. Without explicit rules, the AI generates generic TypeScript that compiles but doesn't leverage the framework's type system.
For Next.js: 'Use the Metadata type for generateMetadata return values. Use the correct props type for page components (params is a Promise in the App Router). Use server actions with proper return types, not generic Response objects.'
For Drizzle ORM: 'Use db.select() with explicit column selection instead of select(*). Use the schema-inferred types for insert and select operations. Use eq, and, or from drizzle-orm for query conditions, not raw SQL.'
For tRPC: 'Define input schemas with Zod in the procedure definition. Use the inferred types for client-side consumption. Never bypass the type-safe client with raw fetch calls to tRPC endpoints.'
Complete TypeScript Rules Template
Here's a consolidated template for TypeScript teams. It covers type safety, validation, framework patterns, and the common AI failures that produce code that compiles but doesn't meet production standards.
- Never use `any` — use `unknown` + type guards or create specific types
- Never use type assertions (`as`) — use type guard functions or Zod parsing
- Zod validation at all API boundaries — infer types with z.infer<typeof schema>
- Discriminated unions for variant types — make impossible states unrepresentable
- Named exports only — never default exports (except where framework requires)
- Async/await always — never .then() chains or callbacks
- Strict mode enabled — noImplicitAny, strictNullChecks, noUncheckedIndexedAccess
- Prefer const assertions and satisfies over type annotations where applicable