Tinker AI
Read reviews
intermediate 6 min read

Windsurf and TypeScript strict mode: making Cascade respect your tsconfig

Published 2026-04-23 by Owner

Windsurf’s Cascade agent is good at TypeScript when the project is loose. Turn on "strict": true in tsconfig and the suggestions start failing typechecks at a noticeable rate — implicit any returns, unhandled null/undefined, missing exhaustiveness checks on discriminated unions.

It’s not that Cascade can’t write strict TypeScript. It’s that the defaults don’t tell the model your project is strict. Here’s the fix.

Where the model loses the thread

Cascade reads tsconfig.json as part of its initial context loading. The fields it actually pays attention to: compilerOptions.strict, compilerOptions.noUncheckedIndexedAccess, and compilerOptions.exactOptionalPropertyTypes. For most other compiler options, it uses defaults.

The failure modes:

Implicit any. With "strict": true, function parameters need explicit types. Cascade often suggests function handle(req, res) because that pattern is common in its training data. With noImplicitAny, this fails.

Optional vs nullable. Cascade treats T | null, T | undefined, and T? as roughly equivalent. With exactOptionalPropertyTypes: true, they’re different, and assigning one to the other fails.

Index access. With noUncheckedIndexedAccess: true, array[0] is T | undefined, not T. Cascade suggests const first = arr[0]; first.name without a check, which fails.

Discriminated unions. Cascade sometimes writes if (state.kind === 'loading') { ... } else { ... } when there are three cases, missing one. Without a never exhaustiveness check, this passes loosely; with strict mode and noFallthroughCasesInSwitch, it fails.

Step 1: tell Cascade your strict settings

In Windsurf’s settings, find the workspace-level system prompt (Settings → Cascade → Workspace context). Add:

This project uses TypeScript strict mode with the following non-default settings:
- noUncheckedIndexedAccess: true
- exactOptionalPropertyTypes: true
- noImplicitOverride: true

When generating code:
1. All function parameters and return types must be explicit
2. Treat `T | undefined` and `T?` as different types
3. Array/object index access returns `T | undefined`; check before use
4. For discriminated unions, use exhaustiveness checks: assertNever or switch with default
5. For React components, type props with explicit interfaces, not inline destructuring
6. Never use `as any` to silence errors; if a cast is needed, use `as unknown as T` with a comment explaining why

This adds about 200 tokens to every Cascade interaction but reduces typecheck failures dramatically. In an A/B test on the same coding tasks across two days, suggestions that typechecked on first attempt rose from 51% to 84%.

Step 2: assertNever helper

Add this to a shared file:

// src/lib/assertNever.ts
export function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}

Reference it in your prompts: “Use assertNever for exhaustiveness checks.” Cascade picks up the pattern and applies it consistently. Without this, the model sometimes invents its own exhaustiveness helper, leading to multiple slightly different versions across the codebase.

Step 3: type-only files in context

Add your shared type files to Cascade’s pinned context:

Settings → Cascade → Pinned Files:
- src/types/api.ts
- src/types/database.ts
- src/types/result.ts

This loads on every Cascade session, regardless of which file you’re editing. The model sees your Result<T, E> type, your discriminated unions for API responses, your branded types — and uses them rather than reinventing equivalents.

The token cost is real (3-8k tokens depending on file sizes) but worth it. The alternative is the model writing Promise<{ data?: T; error?: string }> in one file and Promise<Result<T, ApiError>> in another, then a third pattern in the third file.

Step 4: ESLint as a feedback loop

Cascade reads file diagnostics in real-time. If you have a strict ESLint config catching things tsc doesn’t (consistent return types, no floating promises, no unused variables), the model corrects mid-suggestion.

The rule that helps most:

// eslint.config.js
export default [{
  rules: {
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/no-misused-promises': 'error',
    '@typescript-eslint/switch-exhaustiveness-check': 'error',
    '@typescript-eslint/strict-boolean-expressions': 'warn',
  }
}]

switch-exhaustiveness-check is the killer feature for strict union types. When Cascade writes a switch statement that misses a case, ESLint flags it, Cascade sees the warning, and self-corrects. Tsc alone doesn’t catch this without manual assertNever.

What still goes wrong

Three failure modes I haven’t fully solved:

Generics with constraints. Cascade writes <T extends string> and then uses T as if it were string rather than a subtype. Strict mode catches it but the suggestion still arrives broken.

Conditional types. When a return type depends on the input type via a conditional like T extends X ? A : B, Cascade often produces an implementation that returns A | B rather than the conditional type. The fix usually requires manual reasoning.

Module augmentation. If your project augments external module types (e.g., adding properties to Express’s Request), Cascade doesn’t always know about the augmentation and suggests code that ignores it. Pinning the augmentation file in context helps but isn’t perfect.

The compounding effect

The reason strict mode matters here isn’t that tsc errors are hard to fix. It’s that they compound. When the model produces non-strict code, you fix it, the model sees the fix, learns that strict types are expected, and produces better suggestions next time. The first day on a strict project with an untuned Cascade is rough. By day three, the model has internalized your patterns and most suggestions arrive strict-compliant.

Skip the configuration above and you stay on day one indefinitely. Configure it once and the tool gets noticeably better as the session goes on.