---
title: "Claude Code for TypeScript Development: Patterns That Work"
description: "Maximize Claude Code's TypeScript capabilities — type inference, generic patterns, strict mode compliance, Zod schemas, React types, and CLAUDE.md configurations for TS projects."
canonical: https://callsphere.ai/blog/claude-code-typescript-development-guide
category: "Agentic AI"
tags: ["Claude Code", "TypeScript", "React", "Type Safety", "Zod"]
author: "CallSphere Team"
published: 2026-01-18T00:00:00.000Z
updated: 2026-05-07T12:53:19.404Z
---

# Claude Code for TypeScript Development: Patterns That Work

> Maximize Claude Code's TypeScript capabilities — type inference, generic patterns, strict mode compliance, Zod schemas, React types, and CLAUDE.md configurations for TS projects.

## Claude Code and TypeScript: A Natural Fit

TypeScript's explicit type system gives Claude Code more information to work with than dynamically typed languages. When Claude Code reads a TypeScript file, it understands the types, interfaces, and generics — and uses that information to generate code that is type-safe from the start.

This guide covers the TypeScript-specific patterns, configurations, and prompts that get the best results from Claude Code.

## CLAUDE.md for TypeScript Projects

```markdown
# TypeScript Project Configuration

## Compiler Settings
- strict: true (all strict checks enabled)
- noUncheckedIndexedAccess: true
- exactOptionalPropertyTypes: true
- Target: ES2022
- Module: NodeNext

## Type Conventions
- Never use \`any\` — use \`unknown\` and narrow with type guards
- Prefer interfaces for object shapes, types for unions and intersections
- Use branded types for IDs: type UserId = string & { readonly __brand: "UserId" }
- All function parameters and return types must be explicitly annotated
- Use readonly for arrays and objects that should not be mutated

## Import Conventions
- Use type-only imports: import type { User } from "./types"
- No barrel file imports — import from specific modules
- No default exports — always named exports

## Error Handling
- Use a Result type for operations that can fail
- Never throw in service functions — return Result
- Use discriminated unions for error types
```

## Type-Safe API Endpoints

Claude Code generates well-typed API handlers when given clear type requirements:

```mermaid
flowchart LR
    USER(["User message"])
    LOOP{"messages.create
agent loop"}
    THINK["Extended thinking
optional"]
    TOOL{"stop_reason
tool_use?"}
    EXEC["Execute tool
append tool_result"]
    DONE(["stop_reason
end_turn"])
    USER --> LOOP --> THINK --> TOOL
    TOOL -->|Yes| EXEC --> LOOP
    TOOL -->|No| DONE
    style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
    style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style DONE fill:#059669,stroke:#047857,color:#fff
```

```
You: Create a type-safe CRUD API for a "projects" resource using Express and Zod.
Include request validation, response types, and error handling.
```

```typescript
// types/project.ts
import { z } from "zod";

export const CreateProjectSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().max(1000).optional(),
  visibility: z.enum(["public", "private"]).default("private"),
  teamId: z.string().uuid(),
});

export const UpdateProjectSchema = CreateProjectSchema.partial().omit({ teamId: true });

export const ProjectQuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
  visibility: z.enum(["public", "private"]).optional(),
});

export type CreateProjectInput = z.infer;
export type UpdateProjectInput = z.infer;
export type ProjectQuery = z.infer;

export interface Project {
  id: string;
  name: string;
  description: string | null;
  visibility: "public" | "private";
  teamId: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface PaginatedResponse {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}
```

```typescript
// middleware/validate.ts
import { z, ZodSchema } from "zod";
import { Request, Response, NextFunction } from "express";

export function validateBody(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(422).json({
        success: false,
        error: "Validation failed",
        details: result.error.flatten().fieldErrors,
      });
    }
    req.body = result.data;
    next();
  };
}

export function validateQuery(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.query);
    if (!result.success) {
      return res.status(400).json({
        success: false,
        error: "Invalid query parameters",
        details: result.error.flatten().fieldErrors,
      });
    }
    req.query = result.data as any;
    next();
  };
}
```

## Advanced Type Patterns

Claude Code handles complex TypeScript patterns well when the intent is clear.

### Discriminated Unions for Error Handling

```typescript
// Claude Code generates clean Result types
type Result =
  | { success: true; data: T }
  | { success: false; error: E };

type AppError =
  | { code: "NOT_FOUND"; message: string; resource: string }
  | { code: "VALIDATION"; message: string; fields: Record }
  | { code: "UNAUTHORIZED"; message: string }
  | { code: "FORBIDDEN"; message: string }
  | { code: "CONFLICT"; message: string; conflictingField: string };

// Usage in services
async function getProject(id: string): Promise {
  const project = await db.project.findUnique({ where: { id } });
  if (!project) {
    return {
      success: false,
      error: { code: "NOT_FOUND", message: "Project not found", resource: "project" },
    };
  }
  return { success: true, data: project };
}
```

### Generic Repository Pattern

```typescript
// Claude Code generates clean generics when prompted
interface Repository {
  findById(id: string): Promise;
  findMany(query: PaginationQuery): Promise>;
  create(input: CreateInput): Promise;
  update(id: string, input: UpdateInput): Promise;
  delete(id: string): Promise;
}

class PrismaRepository implements Repository {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly model: Model,
  ) {}

  async findById(id: string): Promise {
    return (this.prisma[this.model] as any).findUnique({ where: { id } });
  }

  async findMany(query: PaginationQuery): Promise> {
    const { page, limit } = query;
    const [data, total] = await Promise.all([
      (this.prisma[this.model] as any).findMany({
        skip: (page - 1) * limit,
        take: limit,
      }),
      (this.prisma[this.model] as any).count(),
    ]);
    return {
      data,
      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
    };
  }

  // ... create, update, delete implementations
}
```

### Branded Types for Type-Safe IDs

```typescript
// Prevent mixing up different ID types
declare const brand: unique symbol;
type Brand = T & { readonly [brand]: B };

type UserId = Brand;
type ProjectId = Brand;
type TeamId = Brand;

function createUserId(id: string): UserId {
  return id as UserId;
}

// Now the compiler prevents mixing IDs:
function getProject(id: ProjectId): Promise { /* ... */ }

const userId = createUserId("abc-123");
// getProject(userId);  // TypeScript Error: UserId is not assignable to ProjectId
```

## React + TypeScript Patterns

Claude Code generates well-typed React components:

```typescript
// Generic list component with proper types
interface DataTableProps {
  data: T[];
  columns: ColumnDef[];
  isLoading?: boolean;
  onRowClick?: (row: T) => void;
  emptyMessage?: string;
}

function DataTable({
  data,
  columns,
  isLoading = false,
  onRowClick,
  emptyMessage = "No data found",
}: DataTableProps) {
  if (isLoading) return

| {col.header} |
| --- |
| {col.cell ? col.cell(row) : String(row[col.accessorKey as keyof T])} |

  );
}
```

## Type Inference and Strict Mode

Claude Code respects strict TypeScript settings. When your tsconfig.json has strict mode enabled, Claude Code:

- Never uses `any` (uses `unknown` instead)
- Handles null and undefined explicitly
- Returns correct types from async functions
- Uses proper narrowing instead of type assertions

```typescript
// Claude Code with strict mode — proper null handling
async function getUserEmail(userId: string): Promise {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { email: true },
  });

  // Claude Code does NOT write: return user.email
  // It handles the null case:
  return user?.email ?? null;
}
```

## Common Prompts for TypeScript Work

| Task | Prompt |
| --- | --- |
| Add types to JS file | "Convert utils.js to TypeScript with strict types" |
| Type an API response | "Create TypeScript types for this API response: [paste JSON]" |
| Zod schema from type | "Generate a Zod schema that validates this TypeScript interface" |
| Fix type errors | "Fix all TypeScript errors reported by tsc --noEmit" |
| Generic component | "Create a generic DataTable component that works with any data shape" |
| Type guard | "Write a type guard for the User vs AdminUser discriminated union" |

## Conclusion

Claude Code produces its best TypeScript when you give it a strict tsconfig.json, clear type conventions in CLAUDE.md, and explicit instructions about patterns like Result types, branded IDs, and Zod schemas. The combination of TypeScript's type system and Claude Code's reasoning creates a development experience where type errors are rare and the generated code passes `tsc --noEmit` on the first attempt.

---

Source: https://callsphere.ai/blog/claude-code-typescript-development-guide
