I asked Claude Code to "parse the API response" and got back this:
const data = response.data as any
const user = data.user as any
const name = (data.user as any).name as string
Three as any casts. All type information gone. TypeScript checking rendered meaningless.
This pattern is a bug factory in real projects. A field gets renamed from userName to displayName in the API, but the as any cast silently swallows the mismatch. No compiler error, no runtime error — until a user reports a blank name on their profile.
By default, Claude Code prioritizes "code that runs" over "code that's type-safe." Changing that requires explicit configuration — the kind you set once in CLAUDE.md rather than fight on every prompt.
Here's what you'll learn in this article:
- Why
as anygets generated and how to stop it - How to enforce
tsconfig.jsonstrict settings through CLAUDE.md - The workflow for letting Claude Code design Zod schemas
- A feedback loop that makes Claude auto-fix type errors
- A CLAUDE.md template for consistently type-safe code generation
Why Does Claude Code Overuse any?
There are three main reasons Claude Code reaches for any:
1. It tries to complete code when type information is missing
No API response type defined, incomplete library typings — when Claude doesn't know the type, it fills with any to make the code compile. Finishing the feature takes priority over type correctness.
2. Your prompt doesn't mention type requirements
"Parse this" and "transform this" say nothing about types. Claude focuses on delivering the requested functionality. Type strictness becomes an afterthought.
3. Your project's type configuration isn't being communicated
If Claude Code doesn't know about your tsconfig.json, it doesn't know you're running strict mode. It generates code that would pass a permissive type check.
The solution has three parts: communicate project config upfront, make Claude define types before writing logic, and require type checking as a mandatory step. Once this is set up, the as any count in Claude Code output drops dramatically.
How Do You Enforce tsconfig.json Strict Settings in CLAUDE.md?
Adding the following to CLAUDE.md makes Claude Code reference your type safety requirements before generating any code.
First, the tsconfig.json settings (as of TypeScript 5.9, tsc --init includes noUncheckedIndexedAccess and exactOptionalPropertyTypes by default):
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Then the CLAUDE.md rules:
## TypeScript Configuration
### Code generation rules
1. `as any` is banned. No exceptions.
2. `as Type` casting is only allowed after a type guard or Zod validation
3. All function parameters and return types must have explicit type annotations
4. Use `unknown` instead of `any`, then narrow with type guards
5. Rely on type inference where possible (excessive explicit annotations are also bad)
### Required verification
After generating any code, run `npx tsc --noEmit`.
Fix all type errors before submitting. Do not return code with type errors.
Copying your tsconfig.json into CLAUDE.md is the single highest-leverage change you can make. It tells Claude exactly what constraints it's operating under — no need to repeat "don't use any" on every prompt.
How Do You Let Claude Code Design Your Zod Schemas?
For external API responses, the right order is: define the Zod schema first, then generate the parsing code. Here's how to make that work.
Step 1: Feed Claude an API response sample and get a Zod schema
Generate a Zod schema from the following API response example.
API response example:
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "admin" | "user" | "guest",
"createdAt": "2026-01-01T00:00:00Z",
"profile": {
"bio": "Developer",
"avatarUrl": "https://example.com/avatar.jpg" | null
}
}
Requirements:
- Type every field explicitly
- Define role as a union type
- Convert createdAt to Date using z.coerce.date()
- Mark nullable fields with .nullable()
- Export inferred types using z.infer<typeof Schema>
Generated schema:
import { z } from 'zod'
const ProfileSchema = z.object({
bio: z.string(),
avatarUrl: z.string().url().nullable(),
})
const UserRoleSchema = z.enum(['admin', 'user', 'guest'])
export const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
email: z.string().email(),
role: UserRoleSchema,
createdAt: z.coerce.date(),
profile: ProfileSchema,
})
export type User = z.infer<typeof UserSchema>
export type UserRole = z.infer<typeof UserRoleSchema>
Step 2: Generate the parsing function using the schema
Write a function that fetches user data and parses it with UserSchema above.
Requirements:
- Receive the response as unknown
- Throw ZodError on parse failure
- No `as any` casts
import { UserSchema, type User } from './schemas/user'
export async function fetchUser(userId: number): Promise<User> {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`)
}
const json: unknown = await response.json()
return UserSchema.parse(json)
}
Receiving response.json() as unknown, validating with Zod, and returning a typed value — this is the correct pattern for handling external data without as any.
How Do You Make tsc --noEmit a Hard Requirement in CLAUDE.md?
Add this to CLAUDE.md to prevent Claude from returning code with lingering type errors:
## Mandatory Check Before Submitting Code
After generating or modifying any code, always run `npx tsc --noEmit`.
Fix all type errors until the output shows zero errors.
Never return code that still has type errors.
The "zero errors before submitting" constraint is what makes this work. Without it, Claude sometimes returns code with type errors on the grounds that "it functions correctly." The --noEmit flag runs type checking without emitting JavaScript files — perfect for this kind of gating.
Add it to package.json too:
{
"scripts": {
"type-check": "tsc --noEmit",
"type-check:watch": "tsc --noEmit --watch"
}
}
How Do You Run a Loop That Makes Claude Auto-Fix Type Errors?
Run tsc --noEmit, capture the output, and feed it directly to Claude for automatic fixing.
npx tsc --noEmit 2>&1
Sample output:
src/utils/parseResponse.ts:12:5 - error TS2322: Type 'any' is not assignable to type 'User'.
src/utils/parseResponse.ts:18:3 - error TS7006: Parameter 'data' implicitly has an 'any' type.
Found 2 errors.
Hand this to Claude:
Fix the following TypeScript errors.
Errors:
src/utils/parseResponse.ts:12:5 - error TS2322: Type 'any' is not assignable to type 'User'.
src/utils/parseResponse.ts:18:3 - error TS7006: Parameter 'data' implicitly has an 'any' type.
Fix approach:
- Do not use `as any`
- Use unknown type + type guards, or Zod validation
- Confirm `tsc --noEmit` shows zero errors before responding
Run this loop until the type checker is clean. When there are multiple errors, pass them all at once — Claude will address them together.
How Do You Get Claude Code to Type External API Responses?
When you have a Swagger or OpenAPI spec, passing it to Claude for Zod schema conversion is the fastest approach.
Generate TypeScript + Zod schemas from the following OpenAPI spec.
Spec (excerpt):
---
components:
schemas:
Product:
type: object
required:
- id
- name
- price
properties:
id:
type: integer
name:
type: string
maxLength: 100
price:
type: number
minimum: 0
category:
type: string
enum: [electronics, clothing, food]
stock:
type: integer
nullable: true
---
Requirements:
- Export types using z.infer<typeof Schema>
- Reflect validation constraints (maxLength, minimum) in Zod
- Express nullable with .nullable()
Tools like openapi-zod-client also exist for automated conversion, but having Claude do it gives you more control over edge cases and naming conventions.
How Do You Maximize Type Inference When Using Prisma?
Prisma auto-generates TypeScript types from your database schema. Tell Claude Code to use those types instead of inventing its own. For a deeper dive on Prisma + Claude Code, see Claude Code × Prisma API Auto-Generation.
## Prisma Type Rules (add to CLAUDE.md)
- Actively use Prisma-generated types (PrismaClient, Prisma.UserGetPayload, etc.)
- Import the User type from '@prisma/client', not custom definitions
- Use Prisma.UserGetPayload<{include: {posts: true}}> for query return types
- Do not override Prisma types with custom type definitions
What this looks like in practice:
import type { Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'
type UserWithPosts = Prisma.UserGetPayload<{
include: { posts: { include: { tags: true } } }
}>
export async function getUserWithPosts(userId: string): Promise<UserWithPosts | null> {
return prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
include: { tags: true },
},
},
})
}
When you use Prisma's generated types, database schema changes instantly surface as TypeScript errors. Claude Code just needs "use Prisma types" in the prompt or CLAUDE.md to take advantage of this.
What Does a CLAUDE.md Template for Type-Safe Code Generation Look Like?
Here's the complete CLAUDE.md template based on everything in this article. Copy it into your project as-is.
## TypeScript Type Safety Policy
### Absolute prohibitions
- Using `as any` (banned regardless of circumstances)
- Using `@ts-ignore` (`@ts-expect-error` is allowed with a comment explaining why)
- Function definitions without type annotations (parameters and return types are required)
### Typing external data
- Receive external API/JSON data as `unknown` and validate with Zod
- `response.json() as SomeType` casting is banned
- Export types from Zod schemas using `z.infer<typeof Schema>`
### tsconfig.json compliance
strict: true / noImplicitAny: true / strictNullChecks: true / noUncheckedIndexedAccess: true
### Required check (mandatory)
After generating or modifying code, run `npx tsc --noEmit` and confirm zero errors before responding.
### Zod conventions
- Centralize schema definitions in `src/schemas/`
- `.parse()` throws on validation failure (intentional behavior)
- Use `.safeParse()` when you need to handle errors gracefully
- Always export the inferred type alongside the schema
How Do You Get Accurate Generics and Utility Types?
When Claude Code writes generic types, it tends to overuse unconstrained <T>. Adding generics rules to CLAUDE.md improves precision.
Generics rules for CLAUDE.md:
# Generics Rules
- Always constrain type parameters (`<T extends Base>`, not bare `<T>`)
- Use utility types (Pick, Omit, Partial) actively to reduce new type definitions
- Conditional types max 2 levels deep. Split 3+ level nesting into function overloads
Prompt example for conditional types:
Create an API response type with these conditions:
- GET /users → User[]
- GET /users/:id → User
- POST /users → User
- Use a conditional type that varies the return type per endpoint
Generated type example:
type ApiEndpoint = "GET /users" | "GET /users/:id" | "POST /users";
type ApiResponse<E extends ApiEndpoint> = E extends "GET /users"
? User[]
: E extends "GET /users/:id"
? User
: E extends "POST /users"
? User
: never;
FAQ
Is as unknown as Type safe to use?
It's safer than as any because you're explicitly passing through unknown, but it still bypasses the type checker. The proper approach is to validate with Zod or a type guard before casting. Reserve as unknown as Type for cases where you've already verified the shape at runtime and TypeScript's inference can't follow your logic — and always add a comment explaining why.
What if a third-party library's @types package uses any?
Wrap the library call in a thin adapter function that receives unknown and validates the output with Zod or a type guard. This isolates the any at the boundary instead of letting it leak into your codebase. For example, if someLib.parse() returns any, write function safeParse(input: string): MyType { return MySchema.parse(someLib.parse(input)) }.
Should I use Zod for internal data too, or only external APIs?
For internal data that stays within your TypeScript boundary (e.g., function-to-function calls), TypeScript's static type system is enough. Zod is most valuable at trust boundaries — API responses, user input, environment variables, JSON.parse() output, and anything coming from outside your type-checked code.
How do I type JSON.parse() safely?
JSON.parse() returns any by default. The type-safe pattern is: const raw: unknown = JSON.parse(jsonString) followed by MySchema.parse(raw). This ensures runtime validation before your code touches the data. Never do JSON.parse(str) as MyType.
Does enabling strictNullChecks break existing code?
Almost certainly yes if your project wasn't written with it enabled. Start by enabling it in a separate branch, run tsc --noEmit, and fix errors incrementally. Enabling it typically surfaces numerous null-safety issues that are genuine bugs waiting to happen.
Can I use @ts-expect-error instead of @ts-ignore?
Yes, and you should. @ts-expect-error fails when the error it suppresses gets fixed, so it doesn't silently become dead code. Always add a comment: // @ts-expect-error — library types don't expose internalMethod. Include @ts-expect-error in your CLAUDE.md rules as the only accepted suppression.
What about performance — does strict mode slow down tsc?
Negligibly. The strict flags add more checking passes, but the difference is typically under 5% on projects up to 100k lines. The noUncheckedIndexedAccess flag adds | undefined to every indexed access, which may increase the number of type errors you need to handle — but that's a correctness benefit, not a performance cost.
How do I handle React event types with strict mode?
Use the built-in React event types: React.ChangeEvent<HTMLInputElement>, React.FormEvent<HTMLFormElement>, etc. If Claude Code generates (e: any) =>, add a CLAUDE.md rule: "React event handlers must use typed events from React.*Event. Never type an event parameter as any."
Related Articles
- Claude Code Test Generation Practical Guide — Testing patterns for type-safe code
- Claude Code × Prisma API Auto-Generation — Type-safe API building with Prisma inference
- CLAUDE.md Patterns for Context Management — Embedding type policies in CLAUDE.md
Wrapping Up
The key to getting type-safe TypeScript from Claude Code is making type safety explicit — not an implicit expectation.
- Copy tsconfig.json strict settings into CLAUDE.md — project-wide type policy that sticks
- Require
tsc --noEmitas a mandatory check — prevents type errors from slipping through - Start with Zod schemas for external data — the only way to avoid
as anyon API responses - Feed type error logs directly to Claude — the feedback loop handles auto-fixing
- Tell Claude to use Prisma's generated types — DB schema and TypeScript types stay in sync automatically
Stop saying "don't use any" on every prompt. Write the type policy once in CLAUDE.md and let it do the work.
Related articles: