Skip to content

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:
  1. isolatedModules is now mandatory for any project bundled with Vite, Next.js, esbuild, swc, or Bun. That kills const enum, export =, namespaces, and several reexport patterns that older rules still recommend.
  1. 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 ban as outright in most contexts.
  1. The strict-mode flags have multiplied. noUncheckedIndexedAccess, exactOptionalPropertyTypes, useUnknownInCatchVariables, and noPropertyAccessFromIndexSignature are now standard on modern projects. Cursor rules from 2024 don't know about them and produce code that breaks on the first compile.
The rule below addresses each of these.

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

Sources

  • TypeScript team. "Announcing TypeScript 5.6". Microsoft Dev Blogs. Release notes for disallowed nullish/truthy checks, region-prioritized type display, iterator helpers.

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.

Related topics