Topic · A3
Cursor Rules for TypeScript 5.6: The Modern-Stack Rule Set (2026)
TypeScript 5.6 changed enough that pre-2025 .cursorrules files now produce code your linter rejects. Here are the strict-mode, satisfies-operator, branded-types rules that actually work, with before/after diffs.
# Cursor Rules for TypeScript 5.6: The Modern-Stack Rule Set
Most "TypeScript Cursor rules" on the directories were written against TS 4.9 or 5.2 and never updated. They tell the agent to use enums, allow any as a fallback, and skip satisfies entirely. The result is code your linter rejects the moment it lands.
This page is the corrected rule — written against TypeScript 5.6.3 and the TS 5.5 features that survived adoption (inferred type predicates, ${configDir}, Array.prototype.at). Every directive has a before/after diff showing the code Cursor produces with and without it. The full rule is at the bottom of the page; you can paste it into .cursor/rules/typescript.mdc or use the RuleSell-hosted version with quality scoring and a security scan against the Rules File Backdoor vector.
Why the existing rules are stale
Steve Kinney called Cursor rules a sticky note permanently stapled to every prompt. That's accurate — but only useful if the sticky note reflects the current language. Three structural shifts since TS 5.4 break older rules:isolatedModulesis now mandatory for any project bundled with Vite, Next.js, esbuild, swc, or Bun. That killsconst enum,export =, namespaces, and several reexport patterns that older rules still recommend.
satisfies(TS 4.9) and inferred type predicates (TS 5.5) changed what "type-safe Cursor output" looks like. A 2024-era rule that says "use type assertions sparingly" is no longer specific enough — the new rule has to banasoutright in most contexts.
- The strict-mode flags have multiplied.
noUncheckedIndexedAccess,exactOptionalPropertyTypes,useUnknownInCatchVariables, andnoPropertyAccessFromIndexSignatureare now standard on modern projects. Cursor rules from 2024 don't know about them and produce code that breaks on the first compile.
The six directives that change the most output
1. No any. No as. Narrow with satisfies or unknown + type guards.
Before (Cursor without rule):
``ts
function parseConfig(input: unknown) {
const config = input as { port: number; host: string };
return config;
}
`
After (Cursor with rule):
`ts
type Config = { port: number; host: string };
function isConfig(value: unknown): value is Config {
return (
typeof value === 'object' &&
value !== null &&
'port' in value && typeof (value as Record).port === 'number' &&
'host' in value && typeof (value as Record).host === 'string'
);
}
function parseConfig(input: unknown): Config {
if (!isConfig(input)) throw new Error('Invalid config shape');
return input;
}
`
The rule body says: Never use any. Use unknown and narrow with a type guard or a Zod/Valibot/ArkType schema. The only allowed as is as const, as Type inside a type guard return, or a documented // @ts-expect-error block.
2.
satisfies for inline config objects.
Before:
`ts
const routes: Record = {
list: { method: 'GET' },
create: { method: 'POST' },
};
routes.list.method; // typed as 'GET' | 'POST', not 'GET'
`
After:
`ts
const routes = {
list: { method: 'GET' },
create: { method: 'POST' },
} satisfies Record;
routes.list.method; // narrowly typed as 'GET'
`
The agent now keeps the literal types instead of widening to the contract — which means routes.list.method is 'GET' exactly, not 'GET' | 'POST'. This pays off the most in enum-replacement patterns and route tables.
3. No
enum. Use as const + union.
Before:
`ts
enum UserRole {
Admin = 'admin',
Member = 'member',
}
`
After:
`ts
const UserRole = {
Admin: 'admin',
Member: 'member',
} as const;
type UserRole = (typeof UserRole)[keyof typeof UserRole];
`
The as const version compiles to a plain object with no runtime helper, tree-shakes correctly, and produces the union 'admin' | 'member' for the type. Cursor will produce one of three wrong things without this rule: enum, const enum (breaks isolatedModules), or string union without the runtime constant.
4. Brand IDs at module boundaries.
Before:
`ts
function transfer(fromUserId: string, toOrgId: string, amount: number) { / ... / }
transfer(orgId, userId, 100); // compiles fine, ships a bug
`
After:
`ts
type UserId = string & { readonly __brand: 'UserId' };
type OrgId = string & { readonly __brand: 'OrgId' };
const UserId = (value: string): UserId => value as UserId;
const OrgId = (value: string): OrgId => value as OrgId;
function transfer(fromUserId: UserId, toOrgId: OrgId, amount: number) { / ... / }
transfer(orgId, userId, 100); // ❌ Argument of type 'OrgId' is not assignable to parameter of type 'UserId'
`
The rule adds: All ID-shaped strings (UserId, OrgId, ProjectId, ...) MUST be branded at their module boundary. Constructors are the only allowed location for as Brand. This is the single change that catches the most pre-production bugs in agent-generated code.
5.
noUncheckedIndexedAccess honesty.
When noUncheckedIndexedAccess: true (which the rule mandates), array[0] is T | undefined, not T. Cursor will instinctively use the non-null assertion ! to make the error go away. The rule says: No non-null assertions. Use a guard, ?? default, or Array.prototype.at(-1) with explicit undefined handling.
`ts
// ❌ Banned
const first = items[0]!;
// ✅ Required
const first = items[0];
if (first === undefined) throw new Error('items is empty');
// ✅ Or
const first = items[0] ?? defaultItem;
`
6. Errors are
unknown, not any.
With useUnknownInCatchVariables: true, catch (e) types e as unknown. The rule body adds: Inside catch, narrow with e instanceof Error, isHttpError(e), or your project's error type guard. Never re-assert e as Error.
The full rule (paste into
.cursor/rules/typescript.mdc)
`mdc
description: TypeScript 5.6 strict-mode rules for modern application code.
globs:
- "*/.ts"
- "*/.tsx"
- "*/.mts"
- "*/.cts"
alwaysApply: false
# TypeScript 5.6 Rules
Strict-mode contract
Assume tsconfig.json has all of:
strict: true
noUncheckedIndexedAccess: true
exactOptionalPropertyTypes: true
useUnknownInCatchVariables: true
noPropertyAccessFromIndexSignature: true
isolatedModules: true
verbatimModuleSyntax: true
If a fix you write would require disabling any of these, stop and ask.
Banned constructs
any — never. Use unknown + narrow.
as Type outside three contexts: as const, type-guard return position, JSON parsing.
! non-null assertion — never. Guard, default, or throw with a message.
enum and const enum — replace with as const + union.
namespace and module declarations in app code.
Function type — use a concrete signature.
Object type — use Record or a concrete shape.
Required patterns
- Inline config objects MUST use
satisfies T to preserve literal types.
- ID-shaped strings (UserId, OrgId, etc.) MUST be branded at their module boundary.
- Catch clauses MUST narrow
unknown before using the error.
- Discriminated unions over optional booleans for state machines.
import type for type-only imports (required by verbatimModuleSyntax).
- Prefer
Array.prototype.at(-1) with explicit undefined handling over array[array.length - 1].
When the user asks for "a quick fix"
Refuse to weaken types. Offer two options: (a) a type guard, (b) explicit error handling. Never offer as any as option (c), even with a comment.
`
Where this rule fails
We were going to write a strengths section. The failure modes are more useful:
1. The agent ignores the rule on long edits. Once the conversation passes ~30k tokens, Cursor starts dropping rule directives mid-file. The forum has dozens of threads on this. The mitigation is to keep the rule under 500 lines (Cursor's documented limit) and to use mode: agent frontmatter so the rule is injected fresh per request, not just at session start.
2. .mdc glob matching is flaky. Cursor 2.0 project rule MDC files with glob patterns not auto-loaded is a known issue. If you switch to .mdc and the rule stops applying, fall back to alwaysApply: true and accept the token cost (~600 tokens per request for this rule, per the developertoolkit.ai cost analysis).
3. Branded types fight ORMs. Drizzle, Prisma, and Kysely all return raw string IDs. The rule forces you to brand at the boundary, which means a wrapper function for every read. That's correct but tedious — be honest with your team about the cost before mandating it.
4. Older tsconfig.json won't honor the contract. The strict-mode contract at the top of the rule assumes a fully modern config. If your project still has strict: false somewhere in the chain, the rule will generate code that would be safe but compiles to nothing useful. Run npx tsc --showConfig before adopting.
What to read next
- /topic/cursor-rules-nextjs-15 — the React framework rules that pair with this TypeScript rule
- /topic/cursor-rules-react-vite-tanstack — for the non-Next.js React stack that nobody on cursor.directory covers
- /topic/claude-md — the same six directives translated to Claude Code's format
- /topic/agents-md — and to the cross-tool AGENTS.md standard
- /for/awesome-cursorrules — install the canonical 39.5k-star collection
- /for/awesome-cursor-rules-mdc — and the 879-file .mdc variant
Sources
- TypeScript team. "Announcing TypeScript 5.6". Microsoft Dev Blogs. Release notes for disallowed nullish/truthy checks, region-prioritized type display, iterator helpers.
- Cursor team. Cursor Rules documentation. Official 500-line rule limit and
.mdc frontmatter spec.
- Kinney, Steve. "Cursor rules for TypeScript". The "sticky note stapled to every prompt" framing and the no-
any directive.
- Total TypeScript team. Style guide and enum-replacement rationale. Reference for the
as const + union pattern.
awesome-cursorrules repository. 39.5k stars; CC0. The category structure RuleSell mirrors.
- Forum thread. "Cursor rules not getting applied". The flakiness problem this rule's
alwaysApply: false` design tries to dodge.
- Forum thread. "Cursor 2.0: Project rule MDC files with glob patterns not auto-loaded". Documented .mdc glob regression.
- developertoolkit.ai. "Cursor IDE — Rules token economics". Source for the "always-apply rule with 100 lines consumes approximately 500-1,000 tokens" figure.
- The Hacker News. "Cursor AI Code Editor Vulnerability — Rules File Backdoor". The prompt-injection attack RuleSell scans for; relevant because rule files run unrestricted at agent context-build time.
Related GitHub projects
Frequently asked
- Do TypeScript 5.6 cursor rules differ from older .cursorrules files?
- Yes, materially. TypeScript 5.6 introduced disallowed nullish/truthy checks, region-prioritized type display, and stricter iterator behavior — and TS 5.5 already shipped inferred type predicates and the `${configDir}` tsconfig token. Cursor rules written before mid-2025 either don't mention these or actively contradict them (for example, telling the agent to widen with `as any` instead of narrowing with `satisfies`). The rule below has been rewritten against the 5.6.3 release notes.
- Should I ban `enum` in a Cursor rule?
- For application code, yes — `const enum` is incompatible with `isolatedModules` (which Next.js, Vite, and esbuild all require), and regular `enum` emits runtime objects that fight tree-shaking. The current community consensus, echoed in the Total TypeScript style guide and several `cursor.directory` entries, is to use `as const` object literals plus a union type. The rule body below shows the diff.
- What is a branded type and why force it in a Cursor rule?
- A branded type is a nominal-typed primitive — for example `type UserId = string & { readonly __brand: 'UserId' }`. Without the rule, Cursor will happily swap `UserId` and `OrgId` because both are strings to the structural type system. The rule below tells Cursor to brand IDs at module boundaries and validate at construction. This is the single rule that prevents the largest class of silent ID-mixup bugs in agent-generated code.
- Why is `satisfies` better than `as` for Cursor-generated code?
- `as` is a type assertion — it tells TypeScript to shut up. `satisfies` is a type check — it validates the value matches the type but preserves the narrower inferred type. Agents reach for `as` constantly because it makes errors disappear; the rule below makes that a forbidden pattern except in three named cases (testing, JSON.parse boundaries, and DOM event narrowing).
- Does this rule work for both `.cursorrules` and `.mdc`?
- Yes. The rule body is the same. The .mdc version adds frontmatter (`description`, `globs: ['**/*.ts', '**/*.tsx']`, `alwaysApply: false`) so Cursor only loads it when a TS file is in context — this saves ~600 tokens per request on non-TS work. The .cursorrules version is always-on and costs those tokens every call.