By Sagar Shankaran, Founder of CallSphere
Maximize Claude Code's TypeScript capabilities — type inference, generic patterns, strict mode compliance, Zod schemas, React types, and CLAUDE.md configurations for TS projects.
Key takeaways
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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
# 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<T, AppError>
- Use discriminated unions for error types
Claude Code generates well-typed API handlers when given clear type requirements:
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>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.
// 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<typeof CreateProjectSchema>;
export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>;
export type ProjectQuery = z.infer<typeof ProjectQuerySchema>;
export interface Project {
id: string;
name: string;
description: string | null;
visibility: "public" | "private";
teamId: string;
createdAt: Date;
updatedAt: Date;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// middleware/validate.ts
import { z, ZodSchema } from "zod";
import { Request, Response, NextFunction } from "express";
export function validateBody<T extends ZodSchema>(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<T extends ZodSchema>(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();
};
}
Claude Code handles complex TypeScript patterns well when the intent is clear.
// Claude Code generates clean Result types
type Result<T, E = AppError> =
| { success: true; data: T }
| { success: false; error: E };
type AppError =
| { code: "NOT_FOUND"; message: string; resource: string }
| { code: "VALIDATION"; message: string; fields: Record<string, string[]> }
| { code: "UNAUTHORIZED"; message: string }
| { code: "FORBIDDEN"; message: string }
| { code: "CONFLICT"; message: string; conflictingField: string };
// Usage in services
async function getProject(id: string): Promise<Result<Project>> {
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 };
}
// Claude Code generates clean generics when prompted
interface Repository<T, CreateInput, UpdateInput> {
findById(id: string): Promise<T | null>;
findMany(query: PaginationQuery): Promise<PaginatedResponse<T>>;
create(input: CreateInput): Promise<T>;
update(id: string, input: UpdateInput): Promise<T>;
delete(id: string): Promise<void>;
}
class PrismaRepository<
T,
CreateInput,
UpdateInput,
Model extends keyof PrismaClient,
> implements Repository<T, CreateInput, UpdateInput> {
constructor(
private readonly prisma: PrismaClient,
private readonly model: Model,
) {}
async findById(id: string): Promise<T | null> {
return (this.prisma[this.model] as any).findUnique({ where: { id } });
}
async findMany(query: PaginationQuery): Promise<PaginatedResponse<T>> {
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
}
// Prevent mixing up different ID types
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type ProjectId = Brand<string, "ProjectId">;
type TeamId = Brand<string, "TeamId">;
function createUserId(id: string): UserId {
return id as UserId;
}
// Now the compiler prevents mixing IDs:
function getProject(id: ProjectId): Promise<Project> { /* ... */ }
const userId = createUserId("abc-123");
// getProject(userId); // TypeScript Error: UserId is not assignable to ProjectId
Claude Code generates well-typed React components:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
// Generic list component with proper types
interface DataTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
isLoading?: boolean;
onRowClick?: (row: T) => void;
emptyMessage?: string;
}
function DataTable<T extends { id: string }>({
data,
columns,
isLoading = false,
onRowClick,
emptyMessage = "No data found",
}: DataTableProps<T>) {
if (isLoading) return <TableSkeleton columns={columns.length} />;
if (data.length === 0) return <EmptyState message={emptyMessage} />;
return (
<table className="w-full">
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.accessorKey)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr
key={row.id}
onClick={() => onRowClick?.(row)}
className={onRowClick ? "cursor-pointer hover:bg-gray-50" : ""}
>
{columns.map((col) => (
<td key={String(col.accessorKey)}>
{col.cell ? col.cell(row) : String(row[col.accessorKey as keyof T])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Claude Code respects strict TypeScript settings. When your tsconfig.json has strict mode enabled, Claude Code:
any (uses unknown instead)// Claude Code with strict mode — proper null handling
async function getUserEmail(userId: string): Promise<string | null> {
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;
}
| 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" |
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.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Anthropic unveiled 10 pre-built finance agent templates on May 5, 2026 across pitchbook building, KYC screening, and month-end close. What each template does and the hours it replaces.
Anthropic shipped finance plugins for Claude Cowork and Claude Code on May 5, 2026. How analysts use them in practice and what the plugin model means for adoption.
AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.
Mastra.ai is becoming the go-to TypeScript agent framework in 2026. Workflows, RAG, evals, and an honest comparison with Vercel AI SDK 5 for serious teams.
Enterprise CIO Guide perspective on Claude Code 2.1 ships background agents, sub-agent spawning, and a hooks API that turn it into a true multi-agent coding platform.
Pair Claude Code with Code-Review-Graph and you have a local-first agentic IDE with deterministic context, blast radius PR review, and zero per-seat indexing fees.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.