Tips & Tricks (Updated: 6/3/2026)

Claude Code and TypeScript: Practical Tips for Safer Speed

Use strict config, Zod, unions, generics, satisfies, and type tests to make Claude Code safer.

Claude Code and TypeScript: Practical Tips for Safer Speed

Claude Code can make TypeScript work feel fast, especially when you are building forms, API helpers, and tests. The catch is that it can also create fragile code quickly when type boundaries are vague. For beginners, the safest habit is simple: define the guardrails before asking Claude Code to generate the feature.

In this article, strict means “TypeScript refuses suspicious code”, domain types mean “business rules written as types”, a discriminated union means “a state model where each state has its own shape”, and runtime validation means “checking data while the program runs”. The goal is not advanced type gymnastics. The goal is copy-pasteable TypeScript that gives Claude Code less room to invent unsafe shortcuts.

Give Claude Code a Type Map First

Start with a small map of the work: compiler rules, domain types, external inputs, state, and type tests. That map makes the generated diff easier to review.

flowchart TD
  A["Requirement"] --> B["tsconfig: strict rules"]
  B --> C["Domain types: Plan and Account"]
  C --> D["External data: unknown then validate"]
  D --> E["State: discriminated unions"]
  E --> F["Type tests: expectTypeOf / tsd"]
  F --> G["Claude Code implementation and review"]

Use official TypeScript references as the baseline: strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, Narrowing, Generics, Utility Types, and the satisfies operator note. For runtime validation, keep the Zod docs open as well.

Related ClaudeCodeLab reads: TypeScript Utility Types, TypeScript Generics, and Zod Validation.

Start With a Strict tsconfig

Do not only say “build this in TypeScript”. Give Claude Code the compiler contract first.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"]
}

Then put the rules into the prompt.

This repository uses strict TypeScript.
Do not introduce any. Treat external input as unknown and validate it with Zod.
When handling unions in switch statements, add a never exhaustiveness check.
After implementation, run npx tsc --noEmit.

noUncheckedIndexedAccess keeps undefined visible when reading arrays and records. It may feel strict at first, but it catches missing API fields, empty lists, and incomplete CMS locale data before runtime.

Use Case 1: Model SaaS Plans as Domain Types

Domain types are business rules written in TypeScript. Plans, permissions, billing states, and publishing states should exist before the UI code.

export type Plan = "free" | "pro" | "enterprise";

export type Account = {
  id: string;
  email: string;
  plan: Plan;
  seats: number;
  trialEndsAt: string | null;
};

export type CreateAccountInput = {
  email: string;
  plan: Exclude<Plan, "enterprise">;
  seats?: number;
};

export type UpdateAccountInput = Partial<
  Pick<Account, "email" | "plan" | "seats" | "trialEndsAt">
>;

Exclude removes members from a union. Partial makes properties optional, which is useful for update APIs but dangerous for create APIs if used too broadly.

Use Case 2: Validate API Data From unknown

TypeScript types disappear at runtime. Data from APIs, forms, cookies, local storage, CSV files, and AI output can be broken. Accept those values as unknown, validate them, and only then use the typed result.

npm install zod
import { z } from "zod";

const AccountSchema = z.object({
  id: z.string().min(1),
  email: z.string().email(),
  plan: z.enum(["free", "pro", "enterprise"]),
  seats: z.number().int().positive(),
  trialEndsAt: z.string().datetime().nullable()
});

type Account = z.infer<typeof AccountSchema>;

export function parseAccountResponse(json: unknown): Account {
  return AccountSchema.parse(json);
}

unknown means the value has not been proven yet. Unlike any, it forces validation before property access.

Use Case 3: Keep Payment State Closed

Payments, uploads, forms, and background jobs are state machines. Avoid status: string; use a discriminated union.

type PaymentResult =
  | { status: "pending"; invoiceId: string }
  | { status: "paid"; invoiceId: string; paidAt: string }
  | { status: "failed"; invoiceId: string; reason: string };

export function renderPaymentMessage(result: PaymentResult): string {
  switch (result.status) {
    case "pending":
      return `Invoice ${result.invoiceId} is waiting for payment.`;
    case "paid":
      return `Invoice ${result.invoiceId} was paid at ${result.paidAt}.`;
    case "failed":
      return `Invoice ${result.invoiceId} failed: ${result.reason}.`;
    default: {
      const exhaustive: never = result;
      return exhaustive;
    }
  }
}

The never branch tells TypeScript that every valid case should already be handled. If a later refunded state is added, the compiler forces the missing branch to be written.

Use Case 4: Generics and satisfies for Reusable Helpers

Generics make helpers reusable while preserving the specific type at each call site.

export function groupBy<T, K extends PropertyKey>(
  items: readonly T[],
  getKey: (item: T) => K
): Partial<Record<K, T[]>> {
  const grouped: Partial<Record<K, T[]>> = {};

  for (const item of items) {
    const key = getKey(item);
    const bucket = grouped[key] ?? [];
    bucket.push(item);
    grouped[key] = bucket;
  }

  return grouped;
}

const accounts = [
  { id: "a1", plan: "free" },
  { id: "a2", plan: "pro" },
  { id: "a3", plan: "pro" }
] as const;

const byPlan = groupBy(accounts, (account) => account.plan);
const proAccounts = byPlan.pro ?? [];

console.log(proAccounts.map((account) => account.id));

For configuration objects, prefer satisfies over a broad assertion.

type ApiRoute = {
  method: "GET" | "POST" | "PATCH" | "DELETE";
  path: `/${string}`;
  auth: boolean;
};

const routes = {
  listAccounts: { method: "GET", path: "/accounts", auth: true },
  createAccount: { method: "POST", path: "/accounts", auth: true },
  healthCheck: { method: "GET", path: "/health", auth: false }
} as const satisfies Record<string, ApiRoute>;

type RouteName = keyof typeof routes;

export function getRoute(name: RouteName) {
  return routes[name];
}

Ask Claude Code to use satisfies for route maps, feature flags, pricing tables, and design tokens.

Add Type-Level Tests

Important exported types deserve tests. Vitest’s expectTypeOf is practical inside a normal test suite.

npm install -D vitest tsd
import { expectTypeOf, test } from "vitest";

type CreateAccountInput = {
  email: string;
  plan: "free" | "pro";
  seats?: number;
};

test("CreateAccountInput keeps the public API narrow", () => {
  expectTypeOf<CreateAccountInput>().toMatchTypeOf<{
    email: string;
    plan: "free" | "pro";
    seats?: number;
  }>();
});

With tsd, you can also keep calls that should fail.

import { expectError, expectType } from "tsd";
import { renderPaymentMessage } from "./payment";

expectType<string>(renderPaymentMessage({
  status: "pending",
  invoiceId: "inv_001"
}));

expectError(renderPaymentMessage({
  status: "refunded",
  invoiceId: "inv_001"
}));

Practical Use Cases and Pitfalls

Use caseWhat the type system ownsWhat Claude Code can generate
SaaS billingplans, invoice states, permissionsUI branches, forms, messages
Admin API screensZod schemas, response typesfetch helpers, tables, loading states
Article CMSslug, locale, publish state, hero imageMDX drafts, listings, validation fixes
Contact formsinput schema, submission result unionUI, submit handler, Vitest coverage
PitfallFailureFix
API response typed as anybroken JSON still compilesaccept unknown, validate with Zod
status: stringimpossible states appearuse a discriminated union
frequent as Usererrors are hiddenuse schemas, guards, or satisfies
Partial<T> for create inputrequired fields become optionalseparate create and update types
no type testsexported types widen silentlyadd expectTypeOf or tsd

In ClaudeCodeLab article workflows, keeping lang, slug, heroImage, and publish state as narrow types prevented invalid localized URLs from being generated. That is the practical lesson: the narrower the boundary, the better Claude Code’s edits become. Before asking for a large rewrite, choose one boundary and write the expected type behavior in plain English. A good request says which value is external, which state must be closed, and which exported type must not widen. That context lets Claude Code fix the real risk instead of only making TypeScript quieter.

CLAUDE.md Rules and CTA

## TypeScript rules
- Use strict TypeScript.
- Do not introduce `any`. Use `unknown` at external boundaries.
- Prefer discriminated unions for states.
- Prefer `satisfies` over broad type assertions.
- Derive API types from Zod schemas when runtime data is involved.
- Add Vitest or tsd style type checks for exported helper types.
- Run `npx tsc --noEmit` before reporting completion.

For solo projects, ClaudeCodeLab’s product catalog collects templates and checklists for Claude Code workflows. For teams, Claude Code training and consulting can cover strict migration, Zod boundaries, type tests, and practical CLAUDE.md rules inside a real repository.

Verification Note

I tested this workflow in a small TypeScript project by replacing any API responses with unknown plus Zod, then asking Claude Code to add missing union branches and expectTypeOf tests. The useful result was not prettier types. It was earlier detection of missing states and invalid property access before code review.

#Claude Code #TypeScript #type safety #development efficiency #frontend
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.