---
name: "TypeScript"
description: "Always-on, stack-neutral TypeScript rules — strictness, typing untrusted input, async correctness and safe narrowing."
applyTo: "**/*.ts,**/*.tsx,**/*.mts,**/*.cts"
---

# TypeScript rules

These apply to TypeScript anywhere — browser, server, mobile, scripts. Framework files
(`react`, `angular`, `react-native`, `nodejs`) add their own rules and do not repeat these.
Security rules live in `secure-coding.instructions.md` and `frontend-security.instructions.md`.

## Compiler settings

- Keep `strict: true`. Each flag it enables catches a class of runtime bug; turning one off to
  clear errors moves the failure to production.
- Turn on `noUncheckedIndexedAccess` for new projects: `arr[0]` and `map[key]` can be `undefined`,
  and pretending otherwise is a common source of crashes.
- Use `isolatedModules` and `verbatimModuleSyntax`, with `import type` for type-only imports, so
  bundlers and transpilers cannot disagree about what is erased.

## Types

- No `any`. Use `unknown` and narrow — `any` disables checking for everything it touches, silently
  and transitively.
- Do not use `as` to silence an error. A type assertion is a claim the compiler cannot verify; if
  you are right, narrow instead, and if you are wrong it fails at runtime. `as const` and
  `satisfies` are fine — they add information rather than override it.
- No non-null `!` to clear a warning. Handle the null case, or narrow with a check that also
  documents the invariant.
- Let inference do local variables; write explicit types on exported functions, public APIs and
  module boundaries, where the type is documentation and a change to it is a breaking change.
- Prefer a discriminated union over an object with many optional fields. "Loading or loaded or
  failed" as one type with a `status` field makes impossible states unrepresentable; four optional
  fields makes them inevitable.
- Prefer a union of literals or a `const` object over `enum`; enums have runtime semantics and
  declaration-merging behaviour that surprise people.
- Mark shared and returned data `readonly` where callers should not mutate it.

## Untrusted input

- Anything crossing a boundary — HTTP response, request body, query string, environment variable,
  `JSON.parse`, message payload, file contents — is `unknown` until validated. An interface
  describes what you *hope* arrived; it checks nothing at runtime.
- Parse it with a schema validator and derive the static type from the schema, so the type and the
  runtime check can never drift apart.
- Validate environment variables once at startup and export the typed result. A config value that
  is `undefined` should stop the process immediately, not surface as a confusing failure later.

## Async

- Never leave a promise floating. An unawaited promise loses its error and its ordering; `await`
  it, return it, or attach explicit handling. Enable `no-floating-promises` in the linter.
- `catch` binds `unknown`. Narrow before use — `instanceof Error` — because anything can be thrown.
- Do not mix `await` in a loop with work that could run concurrently; use `Promise.all` when the
  operations are independent, and keep the loop when they are not.

## Errors and nullability

- Distinguish "absent" from "empty" deliberately, and pick one of `null` or `undefined` per
  codebase rather than both.
- Throw `Error` (or a subclass), never a string, so there is a stack trace.
- Preserve the original error when re-throwing — use `cause` rather than discarding it.

## Modules

- No default exports for anything with a name worth keeping; named exports survive renames, refactors
  and find-all-references.
- Keep `index` barrel files thin, or skip them. Deep barrels create import cycles that appear as
  `undefined` at runtime long after the change that caused them.
