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.
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.
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:
{
"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.
## 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
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.