Claude Code for TypeScript Developers

Strict types, Zod, tRPC, Prisma, Next.js App Router, NestJS, generics, ESM — complete 2026 guide with 40+ prompts.

TypeScript 5.x strict mode Zod tRPC Prisma Next.js App Router NestJS Vitest ESM

Why TypeScript Developers Love Claude Code

0
any types introduced (by policy)
40+
copy-paste TypeScript prompts
100%
tsconfig-aware generation
z.infer
Zod schemas + types co-generated

TypeScript's type system is one of the most complex in mainstream programming — conditional types, mapped types, template literal types, infer, and variance annotations require deep knowledge to use correctly. Claude Code understands all of it. It reads your tsconfig.json, knows your strict settings, and generates code that passes the type-checker without casts.

For full-stack TypeScript stacks (Next.js + tRPC + Prisma + Zod), Claude navigates the entire type chain — from database schema to API layer to React component props — keeping types consistent end-to-end.

CLAUDE.md Setup for TypeScript

# TypeScript Project

## Stack
- TypeScript 5.x (strict)
- Framework: Next.js 15 App Router / NestJS 10 / Express
- ORM: Prisma 6 (schema at prisma/schema.prisma)
- Validation: Zod (schemas in src/schemas/)
- API: tRPC v11 (routers in src/server/routers/)
- Testing: Vitest + Testing Library

## Commands
- Type-check: pnpm tsc --noEmit
- Build: pnpm build
- Test: pnpm vitest run
- Lint: pnpm eslint . --ext .ts,.tsx

## Type Policy
- NEVER use `any` — use `unknown` and narrow, or define a proper type
- Prefer `z.infer` over manual interface duplication
- Use strict null checks — never use non-null assertion (!) except in tests
- Throw typed Error subclasses, never raw strings

## tsconfig paths
- @/* maps to ./src/*
- @db/* maps to ./prisma/generated/*
Tip: Paste your full tsconfig.json compilerOptions block into CLAUDE.md. Claude reads it to know whether noUncheckedIndexedAccess is on (affecting array access types), whether exactOptionalPropertyTypes is active, and which module resolution mode you're using.

Type System — Generics, Conditionals, and Utility Types

Generic Functions with Constraints

// Prompt: "Write a generic groupBy function that groups an array of objects
// by a key, preserving the value types exactly."
function groupBy<T, K extends keyof T>(
  arr: T[],
  key: K
): Map<T[K], T[]> {
  return arr.reduce((map, item) => {
    const k = item[key];
    return map.set(k, [...(map.get(k) ?? []), item]);
  }, new Map<T[K], T[]>());
}

// Usage is fully typed — no assertions
const grouped = groupBy(users, 'role'); // Map<'admin'|'user', User[]>

Conditional Types and infer

// Prompt: "Write a utility type that extracts the resolved value
// from a Promise, or returns T if not a Promise."
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

// Prompt: "Create a DeepReadonly type that recursively marks every
// property and array element as readonly."
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends (infer U)[]
    ? ReadonlyArray<DeepReadonly<U>>
    : T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

Mapped Types and Template Literals

// Prompt: "Write a mapped type that converts all methods of a class
// to their async equivalents."
type Asyncify<T> = {
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
    ? (...args: A) => Promise<R>
    : T[K];
};

// Prompt: "Create event-name types from an object of event handlers."
type EventNames<T extends Record<string, unknown>> =
  `on${Capitalize<string & keyof T>}`;
Tip: When you're stuck on a complex type, describe what you want in plain English: "A type that takes an object and makes all keys that hold a string type optional." Claude will produce the correct mapped type with conditional filtering — faster than hunting through TS documentation.

Zod — Runtime Validation + Type Inference

Schema Design Patterns

// Prompt: "Generate a Zod schema for a user registration form with
// password confirmation, email normalization, and age gate."
const RegisterSchema = z.object({
  email: z.string().email().toLowerCase().trim(),
  password: z.string().min(8).max(128),
  confirmPassword: z.string(),
  birthYear: z.number().int().max(new Date().getFullYear() - 18, {
    message: 'Must be 18 or older',
  }),
}).refine(d => d.password === d.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
});

export type RegisterInput = z.infer<typeof RegisterSchema>;

Discriminated Unions with Zod

// Prompt: "Create a discriminated union schema for API responses —
// success has data, error has code and message."
const ApiResponseSchema = z.discriminatedUnion('status', [
  z.object({
    status: z.literal('success'),
    data: z.unknown(),
  }),
  z.object({
    status: z.literal('error'),
    code: z.enum(['NOT_FOUND', 'UNAUTHORIZED', 'VALIDATION_ERROR']),
    message: z.string(),
  }),
]);

export type ApiResponse = z.infer<typeof ApiResponseSchema>;

Transforming External Data

// Prompt: "Parse an ISO date string from a JSON API and convert it
// to a Date object, failing if invalid."
const EventSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1).max(200),
  startAt: z.string().datetime().transform(s => new Date(s)),
  tags: z.array(z.string()).default([]),
});

Framework-Specific Patterns

Next.js App Router TypeScript Patterns

// Prompt: "Create a typed product detail page with async params,
// generateStaticParams, and Suspense streaming."

// app/products/[slug]/page.tsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';

interface PageProps {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ tab?: string }>;
}

export async function generateStaticParams() {
  const products = await db.product.findMany({ select: { slug: true } });
  return products.map(p => ({ slug: p.slug }));
}

export default async function ProductPage({ params, searchParams }: PageProps) {
  const { slug } = await params;
  const product = await db.product.findUnique({ where: { slug } });
  if (!product) notFound();
  return (
    <main>
      <h1>{product.title}</h1>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={product.id} />
      </Suspense>
    </main>
  );
}
// Prompt: "Create a Route Handler for POST /api/subscribe that
// validates with Zod, returns typed JSON errors."

// app/api/subscribe/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';

const Body = z.object({ email: z.string().email() });

export async function POST(req: Request) {
  const json = await req.json();
  const result = Body.safeParse(json);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.flatten() },
      { status: 422 }
    );
  }
  // ... subscribe logic
  return NextResponse.json({ ok: true });
}

tRPC End-to-End Type Safety

// Prompt: "Add a tRPC router for posts with list, byId, and create
// procedures — create is protected and validates with Zod."

// server/routers/posts.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

const CreatePostInput = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
  tags: z.array(z.string()).max(5).default([]),
});

export const postsRouter = router({
  list: publicProcedure
    .input(z.object({ limit: z.number().min(1).max(100).default(20) }))
    .query(async ({ ctx, input }) => {
      return ctx.db.post.findMany({
        take: input.limit,
        orderBy: { createdAt: 'desc' },
        include: { author: { select: { name: true } } },
      });
    }),

  byId: publicProcedure
    .input(z.string().uuid())
    .query(async ({ ctx, input }) => {
      const post = await ctx.db.post.findUniqueOrThrow({ where: { id: input } });
      return post;
    }),

  create: protectedProcedure
    .input(CreatePostInput)
    .mutation(async ({ ctx, input }) => {
      return ctx.db.post.create({
        data: { ...input, authorId: ctx.session.user.id },
      });
    }),
});
// Client usage — fully typed, no codegen needed
const { data: posts } = trpc.posts.list.useQuery({ limit: 10 });
const createPost = trpc.posts.create.useMutation({
  onSuccess: () => utils.posts.list.invalidate(),
});

NestJS TypeScript Patterns

// Prompt: "Create a NestJS users module with a service, controller,
// and DTOs — use class-validator and Swagger decorators."

// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export enum UserRole { Admin = 'admin', User = 'user' }

export class CreateUserDto {
  @ApiProperty({ example: 'user@example.com' })
  @IsEmail()
  email: string;

  @ApiProperty({ minLength: 8 })
  @IsString() @MinLength(8)
  password: string;

  @ApiProperty({ enum: UserRole, default: UserRole.User })
  @IsEnum(UserRole)
  role: UserRole = UserRole.User;
}

// users/users.service.ts
@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  async create(dto: CreateUserDto): Promise<User> {
    const hashed = await bcrypt.hash(dto.password, 12);
    return this.prisma.user.create({
      data: { email: dto.email, passwordHash: hashed, role: dto.role },
    });
  }
}

Prisma Type-Safe Queries

// Prompt: "Write a Prisma query that fetches a user with their last 10
// posts, comment counts, and total post views — one query."
const user = await prisma.user.findUniqueOrThrow({
  where: { id: userId },
  include: {
    posts: {
      take: 10,
      orderBy: { createdAt: 'desc' },
      include: {
        _count: { select: { comments: true } },
      },
    },
    _count: { select: { posts: true } },
  },
});
// user.posts[0]._count.comments — fully typed, no assertions
// Prompt: "Write a Prisma transaction that creates an order, decrements
// inventory, and writes an audit log — rollback on any failure."
const result = await prisma.$transaction(async (tx) => {
  const order = await tx.order.create({ data: orderData });

  const product = await tx.product.update({
    where: { id: orderData.productId },
    data: { inventory: { decrement: orderData.quantity } },
  });

  if (product.inventory < 0) {
    throw new Error('Insufficient inventory');
  }

  await tx.auditLog.create({
    data: { action: 'ORDER_CREATED', orderId: order.id, userId },
  });

  return order;
});

Testing TypeScript with Vitest

Type-Safe Mocks

// Prompt: "Set up Vitest mocks for a service that depends on Prisma
// — mock only the methods used, keep types."
import { vi, describe, it, expect, beforeEach } from 'vitest';
import type { PrismaClient } from '@prisma/client';
import { UsersService } from './users.service';

const mockPrisma = {
  user: {
    findUniqueOrThrow: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
  },
} satisfies Partial<{ user: Partial<PrismaClient['user']> }>;

describe('UsersService', () => {
  let service: UsersService;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new UsersService(mockPrisma as unknown as PrismaClient);
  });

  it('throws when user not found', async () => {
    mockPrisma.user.findUniqueOrThrow.mockRejectedValueOnce(
      new Error('Not found')
    );
    await expect(service.findOne('bad-id')).rejects.toThrow('Not found');
  });
});

API Integration Tests

// Prompt: "Write a Vitest integration test for a tRPC procedure using
// createCallerFactory — no HTTP, fully typed."
import { createCallerFactory } from '@trpc/server';
import { appRouter } from '../server/root';

const createCaller = createCallerFactory(appRouter);

it('creates a post for authenticated user', async () => {
  const caller = createCaller({
    session: { user: { id: 'user-1' } },
    db: testPrisma,
  });

  const post = await caller.posts.create({
    title: 'Hello tRPC',
    content: 'Testing end-to-end without HTTP overhead.',
  });

  expect(post.authorId).toBe('user-1');
  expect(post.title).toBe('Hello tRPC');
});

ESM Migration and Module Resolution

// Prompt: "Update my tsconfig to use NodeNext module resolution
// for ESM, and fix the .js extension import requirement."

// tsconfig.json for ESM (Node 18+)
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "outDir": "dist"
  }
}

// With NodeNext, imports need explicit .js extension (even for .ts files)
import { userService } from './services/users.js'; // ← required for ESM
// Prompt: "Convert this CommonJS package with require/module.exports
// to ESM while keeping CJS compatibility via dual exports."

// package.json dual export map
{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.cjs"
    }
  },
  "main": "./dist/cjs/index.cjs",
  "module": "./dist/esm/index.js"
}

Type-Safe Environment Variables

// Prompt: "Set up type-safe env vars with Zod that fail at startup
// if required vars are missing."

// src/env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  DATABASE_URL: z.string().url(),
  NEXTAUTH_SECRET: z.string().min(32),
  NEXTAUTH_URL: z.string().url(),
  OPENAI_API_KEY: z.string().startsWith('sk-').optional(),
  PORT: z.coerce.number().int().positive().default(3000),
});

export const env = EnvSchema.parse(process.env);
// env.PORT is typed as number, not string
// Missing DATABASE_URL throws at startup with a clear message

40+ TypeScript Prompts for Claude Code

Type inference
"Write a DeepPartial<T> utility type that recursively makes all properties optional, including nested objects and array elements."
Strict migration
"Enable noUncheckedIndexedAccess in tsconfig and fix all resulting type errors in src/ — replace T | undefined with proper guards, never use non-null assertion."
Zod schema
"Generate a Zod schema for this OpenAPI response type, validate all string formats (uuid, email, datetime), and export the inferred TypeScript type."
tRPC procedure
"Add a protected tRPC mutation that validates input with Zod, runs a Prisma transaction, and returns typed output — include optimistic update on the client."
Prisma query
"Write a Prisma query with cursor-based pagination that returns the next N records, the cursor for the next page, and a hasMore boolean — fully typed."
Next.js route
"Create a dynamic Next.js App Router page with typed async params, generateMetadata with opengraph, and an error.tsx boundary that recovers gracefully."
Generic hook
"Write a generic React hook useAsync<T> that wraps any async function, returns {data, loading, error} with correct types, and cancels on unmount."
JS migration
"Migrate src/utils/payments.js to TypeScript. Use unknown instead of any, define typed error classes for each failure mode, preserve the public API signature exactly."
Declaration file
"Write a .d.ts declaration file for this untyped NPM package based on its README and source — add JSDoc, generics where applicable, and an overloaded function signature."
Discriminated union
"Refactor this result object to use a discriminated union (success/failure variants) — update all call sites to use type narrowing, remove all as-casts."
Type guard
"Write a runtime type guard isValidUser(x: unknown): x is User that validates the shape including nested optional fields and an enum role."
Event emitter
"Create a fully typed EventEmitter class where each event name maps to its exact argument types — no any, no string-indexed on/emit."
Builder pattern
"Implement a fluent QueryBuilder<T> class that chains .where(), .select(), .limit(), .orderBy() and infers the return type from .select() fields."
Env validation
"Set up a Zod-validated env.ts that throws at startup if any required env var is missing, with a descriptive error listing every missing var."
React component
"Add strict prop types to this React component — use discriminated unions for conditional props, mark callbacks with correct event types, export Props type."
ESM migration
"Convert this CommonJS library to ESM with a dual CJS/ESM export map in package.json — update all require() to import, add .js extensions for NodeNext resolution."
NestJS module
"Scaffold a NestJS module with controller, service, and repository — use class-validator DTOs, Swagger decorators, and Prisma for persistence."
Vitest mock
"Mock the Prisma client in Vitest using vi.fn() with satisfies for type safety — set up beforeEach cleanup, add a test that verifies a transaction rollback."
Mapped type
"Write a mapped type that produces an object of event listeners from an interface — each key becomes 'onKeyChange', each value becomes a callback receiving the old and new value."
Middleware
"Write a typed Express middleware that reads a JWT, validates with Zod, extends Request with a typed user property, and returns 401 with structured JSON on failure."
Template literals
"Create a template literal type that validates CSS class names against a fixed list of Tailwind prefixes at compile time — no invalid classes allowed."
Singleton service
"Refactor this module-level class to a typed singleton that can be mocked in tests — use a factory function with an injectable dependency interface."
Error classes
"Create a hierarchy of typed Error subclasses for this API — HttpError base with status, then NotFoundError, UnauthorizedError, ValidationError with field details."
Prisma schema
"Add a polymorphic Comment model to schema.prisma that can belong to either a Post or a Video — write the migration, the Prisma query, and the TypeScript type guard."
Exhaustive switch
"Refactor all switch statements over union types to be exhaustive — add a never-typed default that throws with the unhandled variant name."
API client
"Write a type-safe API client class that wraps fetch, parses responses with Zod schemas, and returns typed Result objects — no any, no unchecked JSON.parse."
Opaque types
"Implement opaque/branded types for UserId, PostId, and Email so they can't be mixed accidentally — add factory functions with runtime validation."
tsconfig audit
"Audit my tsconfig.json and list every strict option I haven't enabled, the breaking changes enabling it would cause, and the risk level for enabling it incrementally."
State machine
"Implement a typed state machine for an order status flow — valid transitions must be checked at compile time using conditional types, invalid ones cause a TS error."
Worker threads
"Set up Node.js worker_threads with a typed message protocol — parent and worker both import the same MessageType union, no string-based protocol."

Frequently Asked Questions

Does Claude Code understand TypeScript's type system including generics and conditional types?

Yes — Claude Code has deep understanding of TypeScript's type system. It correctly writes generic functions with constraints, conditional types, mapped types, template literal types, and infer. When you describe a type transformation in plain English, Claude produces the correct type rather than falling back to any. Prompt: "Write a DeepPartial<T> utility type that makes every nested property optional including arrays" — Claude handles the recursive conditional type correctly.

How does Claude Code work with tsconfig and strict mode?

Claude Code produces strictly-typed code by default and understands every tsconfig option. It enables strictNullChecks, noUncheckedIndexedAccess, exactOptionalPropertyTypes, and noImplicitAny, explaining why each matters. Add your tsconfig.json to CLAUDE.md so Claude reads your exact compiler options. Prompt: "Audit my tsconfig.json for strict settings I should enable and explain the breaking changes each would cause."

How does Claude Code handle Zod schemas and runtime validation?

Claude Code treats Zod as the standard runtime validation layer and co-generates the schema and the inferred TypeScript type together using z.infer<typeof Schema>. It writes .transform(), .refine(), and .superRefine() for custom logic, discriminated unions, and schema composition. Prompt: "Generate a Zod schema for this API response type that validates nested arrays, optional fields, and an enum status field, then export the inferred TypeScript type."

Does Claude Code understand tRPC end-to-end type safety?

Yes. Claude Code understands tRPC v10/v11 — router definition, procedure builders, input/output Zod validation, middleware for auth context, and React Query client integration. It correctly threads the Context type and writes createTRPCRouter, publicProcedure, protectedProcedure. Prompt: "Add a tRPC procedure that takes a userId, queries Prisma, applies an auth middleware, and returns typed user data with related posts."

How does Claude Code work with Prisma and type-safe database access?

Claude Code reads your schema.prisma to understand the data model and generates correctly typed Prisma queries — including nested relations with include/select, transactions, and upserts. It knows the difference between findUnique (returns T | null) and findUniqueOrThrow (returns T) and uses the right one for your null-safety requirement. Prompt: "Add a Prisma query that fetches a user with their last 10 posts, ordered by createdAt, with comment counts — all in one query."

How does Claude Code handle Next.js App Router with TypeScript?

Claude Code is current with Next.js App Router: Server vs Client Components ('use client'), async page.tsx with awaited params and searchParams, Route Handlers, generateMetadata, generateStaticParams, and colocated loading.tsx/error.tsx. It correctly types PageProps, LayoutProps, and RouteHandlerContext.

Can Claude Code help migrate a JavaScript project to TypeScript?

Yes — Claude Code is excellent at JS-to-TS migrations. The recommended approach: start with allowJs: true and strict: false, then let Claude migrate one file at a time, adding explicit types rather than any. It generates declaration files for untyped third-party packages and flags ambiguous spots rather than silently casting them. Prompt: "Migrate src/utils/auth.js to TypeScript. Infer argument types from usage context, use unknown instead of any."

What should I put in CLAUDE.md for a TypeScript project?

Essential CLAUDE.md items for TypeScript: (1) TypeScript version, (2) type-check command: tsc --noEmit, (3) test command, (4) lint command with ESLint, (5) key tsconfig paths aliases, (6) ORM and validation library, (7) no-any policy, (8) error handling pattern. Paste your compilerOptions block directly into CLAUDE.md so Claude knows which types are available in your project.

Related Guides

More Claude Code Tools

⚡ Using Claude Code? 30 power prompts that 2× your output · £5 £3 first 10Get PDF £3 →