TypeScript and Cursor is a good combination — the type system gives the model real information to work with instead of guessing. But there are a few configuration choices that make the difference between suggestions that fit your codebase and suggestions that technically compile but fight your patterns.
Give the model your tsconfig
Add to .cursorrules:
TypeScript config: see tsconfig.json. Strict mode is enabled. No implicit any.
Path aliases: @/* maps to ./src/*. Use @/components not ../../../components.
Do not use require(). Use ES module imports only.
Cursor can read tsconfig.json but it doesn't always interpret your path aliases correctly. Stating them explicitly in .cursorrules prevents import path drift.
Type-first development
For any new feature, ask for the types before the implementation:
Before writing any implementation, define the TypeScript types for:
- The input to this function
- The output
- Any intermediate data structures
Show me the types. I'll confirm them before you write the implementation.
Types as a contract up front means the implementation fits your data model rather than the other way around.
Stopping the 'any' creep
Claude will use any when it's uncertain about a type. It's the path of least resistance and the wrong choice for maintainable TypeScript.
Add to CLAUDE.md:
Never use 'any' as a type. If the type is unknown, use 'unknown' and add an explicit type guard. If the type is genuinely variable, use generics.
This forces better type modeling and makes the codebase more maintainable.
Handling generated types
If you're using Prisma, tRPC, or another tool that generates types, tell Cursor where they live:
Generated types:
- Database models: import from '@prisma/client'
- API types: import from '@/server/api/types' (tRPC router types)
Do not manually define types that are already generated.
Without this, Cursor will sometimes create manual interfaces that duplicate your generated types. They get out of sync immediately.
The error annotation pattern
For complex async operations, use typed error returns instead of try/catch everywhere:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
Once you've established this pattern, add it to CLAUDE.md:
For functions that can fail, return Result<T, E> from /src/types/result.ts rather than throwing. Callers should check result.ok before accessing result.value.
Cursor will follow this pattern consistently once it's specified.
Quick win: strict null check discipline
With strict mode on, TypeScript will catch null/undefined access. Claude sometimes works around these with ! non-null assertions instead of proper handling.
Do not use non-null assertion operator (!). If a value can be null or undefined, handle it explicitly. Use type guards or early returns.
Small rule, meaningful improvement in code quality.
.cursorrules templates for TypeScript projects: builtbyzac.com/tools/cursorrules-generator.html.
Top comments (0)