Using Claude Code on TypeScript Projects
Claude Code TypeScript workflows come up constantly because TypeScript is where most production JavaScript work happens now, and the compiler gives an AI coding agent something most languages don't: a fast, deterministic way to check its own work. This article walks through setting Claude Code up on a real TypeScript project, how to get it to verify changes with tsc instead of just guessing, and the specific habits that make a Claude Code TypeScript session productive instead of a loop of half-fixed type errors. It assumes you already have Claude Code installed and are working in a Node-based TypeScript repo, whether that's a single package, a Next.js app, or a pnpm/Turborepo monorepo.
Why TypeScript is a good fit for Claude Code
TypeScript gives an agent a built-in feedback loop that plain JavaScript doesn't have. When Claude Code edits a .ts file, it can immediately run tsc --noEmit and get a structured, file-and-line-numbered list of everything that broke. That loop, edit then compile then fix, is close to how an experienced engineer works, and it's the reason Claude Code tends to do noticeably better on typed codebases than untyped ones. The type checker also catches an entire category of mistakes an agent might otherwise ship silently: wrong argument order, a renamed field that wasn't updated everywhere, an undefined that leaked past a null check.
The tradeoff is that TypeScript projects have more moving parts to get right: tsconfig.json inheritance, path aliases, declaration files, build tooling (tsup, esbuild, swc, plain tsc), and monorepo package boundaries. Claude Code handles all of this fine once it has visibility into the project's actual configuration, but it has to be told where to look and how to verify. That's what the rest of this article covers.
Setting up a TypeScript project for Claude Code
Start with a CLAUDE.md file at the repo root. This is the single highest-leverage thing you can do for any Claude Code TypeScript project, because it removes the need for the agent to re-discover your conventions on every session. A minimal but effective one looks like this:
# Project conventions
- Package manager: pnpm (never npm or yarn)
- Type check: `pnpm tsc --noEmit` from repo root
- Lint: `pnpm eslint . --fix`
- Tests: `pnpm vitest run`
- Strict mode is on. Do not use `any` without a comment explaining why.
- Path alias `@/` maps to `src/`. Use it instead of relative `../../..` imports.
- Shared types live in `packages/types`. Never redefine a type that already exists there.A few things matter here beyond just listing commands. Naming the exact package manager avoids a common failure mode where Claude Code runs npm install in a pnpm workspace and generates a stray package-lock.json that then causes lockfile drift. Pointing at where shared types live stops the agent from creating a second, slightly different User interface in a different file, which is one of the more annoying things to clean up after the fact in a TypeScript codebase.
If your tsconfig.json uses project references ("references": [...]) for a monorepo, mention that too, since it changes which tsc invocation actually checks a given file. Claude Code will read tsconfig.json on its own when it needs to, but a one-line pointer in CLAUDE.md saves it from having to reconstruct that from scratch every time.
Getting Claude Code to verify its own TypeScript changes
The single most important habit in a Claude Code TypeScript session is closing the loop: after every non-trivial edit, run the type checker and let the errors drive the next round of fixes. Don't rely on Claude Code eyeballing a diff and declaring it correct.
In practice this means asking for (or letting the agent settle into) a cycle like:
1. Edit file(s)
2. pnpm tsc --noEmit
3. Read the error list, fix in order, top to bottom
4. Repeat until clean
5. pnpm eslint . --fix
6. pnpm vitest run <affected test files>tsc --noEmit is worth calling out specifically because it's the cheapest possible verification step: it doesn't write build output, doesn't bundle, and on an incremental build with "incremental": true in tsconfig.json it's fast enough to run after nearly every edit. If your project doesn't already have incremental turned on, turning it on is a good investment purely for the Claude Code workflow, since it turns every verification pass from "recompile the world" into "recompile what changed."
A pattern worth adopting explicitly: ask Claude Code to fix one class of error at a time rather than everything at once when the error count is large. If a refactor breaks 40 call sites across a dozen files, having the agent work through them file by file with a tsc run after each file keeps the fixes verifiable and keeps you from ending up with a giant unreviewable diff. It also means if something goes wrong halfway through, you're debugging one file's worth of change, not forty.
Strict mode, `any`, and keeping type safety honest
Claude Code will happily satisfy the type checker by reaching for any or a type assertion (as SomeType) if that's the fastest way to make an error go away. This is the single biggest quality risk in a Claude Code TypeScript session, because it produces code that passes tsc while quietly throwing away the safety you were relying on.
Two things reduce this a lot:
- Turn on
strict: trueintsconfig.jsonif it isn't already. Strict mode (which bundlesstrictNullChecks,noImplicitAny, and friends) forces the agent to actually reason aboutundefinedandnullcases instead of letting them slip through, and it makes lazyanyusage more visible in review. - Add an ESLint rule against unexplained
any, for example@typescript-eslint/no-explicit-anyset to warn, and runeslintas a required step aftertsc. A compiler pass alone won't catchany; it's valid TypeScript. Lint is what catches it.
When you do want Claude Code to use any or an assertion deliberately, for example when typing a third-party library's untyped return value, ask for a one-line comment explaining why. That comment is cheap during the session and saves real time later when someone (possibly a future Claude Code session) is trying to figure out whether that any was intentional or just a shortcut.
It's also worth reviewing generated type definitions the way you'd review generated logic. If Claude Code adds a new interface, check that optional fields (?:) are actually optional in practice, and that union types match the real set of values rather than a convenient subset. Type definitions in TypeScript function as documentation as much as they function as checks, so sloppy ones cost you twice.
Refactoring and migrating JavaScript to TypeScript
Migrating a .js file to .ts (or .jsx to .tsx) is one of the tasks Claude Code handles well, because the compiler tells it exactly what's still wrong after each attempt. The efficient approach is incremental, not a big-bang rewrite:
1. Rename one file: mv src/utils/format.js src/utils/format.ts
2. Run tsc --noEmit and read the errors for that file
3. Add types, starting with function signatures and exported values
4. Leave internal implementation details loosely typed (or `unknown`) if the
effort isn't worth it yet
5. Move to the next fileAsk Claude Code to prioritize exported functions and public interfaces over internal helpers. The types that matter most are the ones at module boundaries, because that's where a wrong type actually causes a bug somewhere else in the codebase. A perfectly typed private helper function that's only called in one place buys you much less than a correctly typed exported API.
For larger migrations, it helps to give Claude Code a batch: "convert these 8 files in src/services/ to TypeScript, keep behavior identical, run tsc --noEmit after each file." That framing keeps the agent from trying to do the whole migration in one giant edit, which is exactly the situation where things go wrong silently, because a compile error 30 files deep is much harder to trace back to its actual cause than one two files in.
The same incremental instinct applies to plain refactors, not just JS-to-TS migrations: renaming a widely-used type, changing a function's parameter shape, splitting one module into two. Let the compiler tell Claude Code the full blast radius of the change (that's what "the errors" mean, structurally: TypeScript's own dependency graph telling you what's affected) rather than asking the agent to grep for callers by hand and hope it found them all.
Monorepos, path aliases, and workspace boundaries
TypeScript monorepos (pnpm workspaces, Turborepo, Nx) add a wrinkle: a single tsc invocation from the repo root might not check every package, and path aliases resolved by your bundler at build time (like Next.js's @/*) aren't always what tsc resolves at type-check time unless tsconfig.json's paths field matches.
A few things to set up explicitly for a monorepo Claude Code TypeScript workflow:
- Confirm each package's
tsconfig.jsonextends a shared base config, and tell Claude Code where that base config lives if it needs to change compiler options project-wide. - If you use project references, tell Claude Code to run
tsc --build(or your framework's equivalent) rather than plaintsc --noEmit, since project references need the build orchestration to check cross-package types correctly. - Make clear which package owns which shared type or utility, so a fix doesn't get duplicated into the wrong package. A one-line map in
CLAUDE.md("API types:packages/api-types. UI components:packages/ui.") does most of the work here. - If Turborepo or Nx caches build/typecheck output, mention the cache-busting command (
turbo run typecheck --force, for example) so Claude Code doesn't get a stale "all clear" from a cached run after making changes.
Getting these boundaries right up front avoids a specific failure mode: Claude Code type-checks only the package it's editing, sees green, and doesn't notice it broke a consumer in a different package that imports the changed type. If your CI runs a full-repo typecheck, it's worth having Claude Code run the equivalent command locally before calling a change done, even if it's slower than checking a single package.
Testing TypeScript changes
Type checking proves the code compiles; it doesn't prove the code does what you meant. Pair tsc --noEmit with your actual test runner, whether that's Vitest, Jest, or something else. For a Claude Code TypeScript session, it helps to be specific about scope:
pnpm vitest run src/services/billing --reporter=verboseRunning the narrow, affected test file first is faster feedback than running the whole suite on every edit, and it keeps the iteration loop tight. Save the full suite run for right before you consider the change finished. If your project uses vitest --watch or Jest's watch mode, those work fine in a Claude Code session too, but a single explicit run per verification step is usually easier to reason about than a persistent watcher, since you get one clear pass/fail result to react to.
For new code, it's reasonable to ask Claude Code to write the test alongside the implementation, using the existing test files in the same directory as the pattern to follow (mock style, assertion library, fixture setup). Pointing at an existing test file as the example is more reliable than describing the testing conventions in prose, because the agent can match the actual style rather than a paraphrase of it.
Common pitfalls in Claude Code TypeScript sessions
A short list of the mistakes that show up repeatedly, and how to head them off:
- Silencing errors instead of fixing them.
@ts-ignoreand@ts-expect-errorcomments are legitimate tools, but they're also the path of least resistance when an agent is trying to get a type error to disappear. Review any new suppression comment as carefully as you'd review a newany. - Editing generated files. Files under
.next/,dist/, or anything with a// AUTO-GENERATEDheader should be excluded from what Claude Code touches. List these paths inCLAUDE.mdor a.claudeignore-style exclusion so the agent isn't tempted to "fix" a generated file that will just get overwritten on the next build. - Trusting a green `tsc` run that only checked one package. Covered above, but worth repeating: in a monorepo, "no errors" only means what it says for the files actually included in that invocation.
- Losing track of which `tsconfig.json` is in effect. A file inside
src/might be governed by a different config than one insidescripts/ortest/if your project excludes or includes paths differently across configs. If Claude Code's fix "works" but the error persists in CI, this mismatch is a common cause. - Over-broad type widening. Fixing a type error by widening a type (turning a specific union into
string, for example) makes the error go away without fixing the actual problem. It's a faster fix than narrowing correctly, which is exactly why an agent under time pressure gravitates to it. Ask for the narrower fix explicitly when you see this happen. - Skipping lint after type check passes.
tscandeslintcatch different things. A file can be fully type-safe and still violate your import order, unused-variable, or React hooks rules. Keep both in the verification loop, not just the compiler.
None of these are unique to AI-assisted coding, they're the same mistakes a rushed engineer makes. The difference is that Claude Code will make them fast and confidently, so the review habits that catch them matter more, not less.
FAQ
Does Claude Code understand TypeScript generics and advanced types? Yes. Claude Code can read and write generic functions, conditional types, mapped types, and utility types (Partial, Pick, Omit, and custom ones) correctly in most cases, especially when there's existing code in the repo using similar patterns it can follow. Complex conditional type gymnastics benefit from being pointed at an existing example rather than described from scratch.
Should I let Claude Code run `tsc --build` or `tsc --noEmit` by default? Use --noEmit for a single package or a simple project, since it's faster and doesn't touch build output. Use --build (with project references) for a monorepo where cross-package type checking matters and where the build graph needs to be respected. Put whichever one applies in CLAUDE.md so it doesn't have to be re-decided every session.
How do I stop Claude Code from adding `any` everywhere? Turn on strict: true in tsconfig.json, add an ESLint rule flagging explicit any, and run lint as a required step in the verification loop, not an optional one. Also just ask directly in the session: tell it any requires a justifying comment, and review any it adds.
Can Claude Code migrate a large JavaScript codebase to TypeScript in one session? It can, but doing it file by file or directory by directory with a tsc --noEmit check after each step produces a much more reviewable result than one giant migration commit. Large one-shot migrations are harder to review and harder to bisect if something breaks.
Does Claude Code work well with Next.js and other TypeScript frameworks? Yes, and the same principles apply: point it at the framework's type-check command (next build includes a type check, or tsc --noEmit on its own for a faster loop), tell it where path aliases and shared types live, and keep generated directories like .next/ out of scope for edits.
What's the fastest way to give Claude Code TypeScript context on a new project? Write a CLAUDE.md with the package manager, the exact type-check and test commands, where shared types and path aliases live, and whether strict mode is on. That single file does more for a Claude Code TypeScript workflow than any amount of describing the project verbally in the session itself.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.