TypeScript code review standards
These TypeScript code review standards cover the places where the type system trusts the code instead of checking it: type assertions, unvalidated external data, non-exhaustive branches over unions, loosely modeled state, and swallowed errors. The compiler in strict mode and the default typescript-eslint presets accept most of these patterns, so they reach production unless a reviewer catches them.
TypeScript agents
- Correctness
Prevent Unsafe Casts and Non-Null Assertions in TypeScript
Catches type assertions, double casts through unknown, non-null assertions, and silenced type errors that hide real bugs in TypeScript.
- Reliability
Validate External Data at Runtime in TypeScript
Flags JSON.parse results, fetch responses, env vars, storage, and postMessage data that TypeScript trusts through a cast or generic instead of a schema.
- Correctness
Require Exhaustive Union Handling in TypeScript
Flags switches and if chains over TypeScript unions that lack a never check, so a newly added variant compiles and is silently ignored or mishandled.
- Code quality
Model State with Discriminated Unions in TypeScript
Flags TypeScript state built from booleans and optional fields, like isLoading plus data?, that allows impossible states a discriminated union rules out.
- Reliability
Prevent Swallowed Errors and Empty Catch Blocks in TypeScript
Flags empty catch blocks, catch-all fallbacks that return null, rethrows without a cause, and non-Error throws that hide failures in TypeScript code.
Also recommended for TypeScript
Agents that apply to any codebase.
Copy all 11 agents
Every agent on this page in one file, ready for your AI coding tool or for cubic.yaml.
AGENTS.md
Add these sections to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
# TypeScript code review standards
Follow these standards when writing or changing code in this repository. Code review flags changes that break them.
## Prevent Unsafe Casts and Non-Null Assertions in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript changes for type escape hatches that make the compiler accept code it cannot verify.
Flag changes that:
- Use `as` to assert a specific type on values from outside the program or on broad types such as `unknown`, `any`, `string`, or `Record<string, unknown>` without a runtime check first.
- Use double assertions such as `as unknown as T` or `as any as T` to force a conversion the compiler rejected.
- Add non-null assertions (`!`) to values that can be null or undefined at runtime, such as `Array.prototype.find` results, `document.querySelector` results, `Map.get` without a prior `has`, or optional fields.
- Write a type predicate (`value is T`) or assertion function (`asserts value is T`) whose body does not check the properties the type claims.
- Silence the checker with an eslint-disable comment for `@typescript-eslint/no-explicit-any` or a `no-unsafe-*` rule, or with `@ts-expect-error`, without a comment that names the specific error and why it cannot be fixed.
Do not flag:
- `as const`, `satisfies`, and widening assertions to a supertype, which claim nothing the compiler has not checked.
- Non-null assertions right after a check the compiler cannot follow, such as `map.get(key)!` following `map.has(key)` in the same scope.
- Assertions in test files and fixtures that build partial objects for mocks.
## Validate External Data at Runtime in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript code that reads data from outside the program for types that are asserted instead of checked at runtime.
Flag changes that:
- Pass the result of `JSON.parse` (which returns `any`) or `response.json()` into typed code through an annotation, an `as` cast, or a generic parameter without validating it with a schema library such as Zod, Valibot, or ArkType, or a type guard that checks each field.
- Add or call generic helpers such as `getJson<T>(url)` that return `T` without taking a schema, so the type parameter works as an unchecked cast.
- Read `process.env` values with `as string`, `!`, or `Number(...)` at use sites instead of parsing required variables once at startup and failing fast when one is missing or malformed.
- Parse values from `localStorage`, `sessionStorage`, cookies, or URL search params and use them as typed objects without handling data written by an older app version or edited by the user.
- Read `event.data` in `window` `message` listeners without checking `event.origin` against an allowlist and validating the payload shape.
- Declare a TypeScript type for an external payload separately from its schema instead of deriving it with `z.infer` or its equivalent in the schema library.
Do not flag:
- Results from typed clients generated from a schema the team controls, such as ORM queries or end-to-end typed RPC within the same repository.
- Values validated earlier in the same flow and passed along with their inferred type.
- Test fixtures and mocks.
## Require Exhaustive Union Handling in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript code that branches on union types for cases the compiler will not report when a new variant is added.
Flag changes that:
- Switch on a union or discriminant such as `kind`, `type`, or `status` in a function that returns `void` or an inferred type, without a `default` branch that assigns the value to `never` or passes it to an `assertNever` helper.
- Add a `default` branch or final `else` over a closed union that returns a fallback, logs, or does nothing, so future variants are handled silently instead of failing to compile.
- Check one or two variants with `if` and treat everything else as the remaining variant, such as `if (status === 'active') { ... } else { ... }` for a union with more than two members.
- Add a variant to an existing union without updating the switches, lookup maps, and filters that branch on it.
- Use `Partial<Record<Union, T>>` or `{ [key: string]: T }` for a per-variant lookup table where `Record<Union, T>` would make a missing entry a compile error.
Do not flag:
- Switches in functions with an explicit non-void return type where every case returns and there is no `default`; with `strictNullChecks`, the compiler already reports a missing case.
- Defaults that deliberately handle values from outside the program, such as webhook event types a provider may add later, as long as they log or report the unknown value.
- Branches where a comment states that every other variant is intentionally handled the same way.
## Model State with Discriminated Unions in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript types for state, results, and domain records that allow combinations of fields that should never exist together.
Flag changes that:
- Model async or request state with separate booleans and optional fields, such as `isLoading`, `isError`, `data?`, and `error?`, where a union keyed on `status` would make invalid combinations unrepresentable.
- Add a `status` or `kind` field but keep variant-specific fields optional at the top level instead of attaching each field to the variant where it exists.
- Return result objects such as `{ ok: boolean; value?: T; error?: string }` instead of `{ ok: true; value: T } | { ok: false; error: string }`.
- Represent mutually exclusive modes with several booleans, such as `isEditing`, `isSaving`, and `isDeleting`, that the code must keep in sync by hand.
- Add optional fields that are only valid together, such as `shippedAt?` and `trackingNumber?`, without grouping them into one variant or one optional object.
- Read fields with `!`, `?.`, or fallbacks such as `?? ''` because the type cannot show which fields exist in the current state.
Do not flag:
- Fields that are independently optional, such as a `nickname?` on a profile, or independent UI toggles such as `isMenuOpen`.
- Types that mirror an external API response or database row at the boundary, as long as the code converts them before other logic uses them.
- State objects returned by third-party libraries; flag only types the diff defines.
## Prevent Swallowed Errors and Empty Catch Blocks in TypeScript
Applies to `**/*.ts`, `**/*.tsx`, `**/*.mts`, `**/*.cts`.
Review TypeScript error handling for failures that are hidden, lose their original cause, or cannot be inspected by callers.
Flag changes that:
- Add `catch` blocks that contain only a placeholder comment such as `// ignore`, which the `no-empty` lint rule accepts, or promise chains ending in `.catch(() => {})` or `.catch(() => undefined)`.
- Return `null`, `undefined`, `[]`, `false`, or another default from a `catch`, so callers cannot tell an outage or a bug from a normal empty result.
- Catch every error when only one is expected, such as treating any failure as "not found" instead of checking for a 404 status or a specific error class.
- Throw a new error inside `catch` without the original as its `cause`: `throw new Error(message)` drops the original stack, while `throw new Error(message, { cause: err })` keeps it.
- Throw or reject with strings, plain objects, or other non-`Error` values, which carry no stack trace and fail `instanceof Error` checks.
- Read `(err as Error).message` on a caught value instead of narrowing with `err instanceof Error`, even though any value can be thrown.
Do not flag:
- Best-effort work where failure is expected and harmless, such as deleting a temp file or sending analytics, when the code logs the error or comments why it is ignored.
- Catches that ignore one specific, checked error, such as an `AbortError` from a canceled request.
- Catches that convert the error into a typed result such as `{ ok: false, error }` that callers must handle.
## Enforce Strict Maintainability Standards
Review changes for structural maintainability regressions. Behavior-correct code can still violate this rule if it makes the codebase harder to understand, extend, or decompose.
Be ambitious about structural simplification. Look for reframings that delete whole branches, helpers, modes, conditionals, or layers instead of adding more of them. Prefer solutions that remove complexity rather than rearrange it.
Flag PRs that:
- Push a file from under 1,000 lines to over 1,000 without a strong reason or clear organization. Prefer extracting helpers, subcomponents, modules, or local abstractions first.
- Add ad-hoc conditionals, scattered special cases, one-off booleans, nullable modes, or flags into already busy shared flows instead of a dedicated abstraction, helper, state machine, policy object, or separate module.
- Introduce thin wrappers, identity abstractions, or pass-through helpers that add indirection without reducing complexity.
- Use brittle, ad-hoc, or "magic" mechanisms that hide simple data-shape assumptions.
- Rely on unnecessary casts, any, unknown, or optional parameters when a clearer type boundary or explicit model would simplify control flow.
- Use silent fallbacks to paper over unclear invariants instead of making the boundary explicit.
- Duplicate existing helpers or put logic in the wrong layer when a canonical utility or module already owns the concept.
- Leak feature-specific logic into general-purpose or shared paths.
- Copy-paste logic instead of extracting a shared helper or pure function.
- Handle narrow edge cases in the middle of an already busy function.
- Refactor in ways that move complexity around without reducing the number of concepts a reader must hold in their head.
- Add "temporary" branching that is likely to become permanent debt.
- Serialize obviously independent work or leave related updates in a non-atomic shape when a cleaner structure is available.
Prioritize findings in this order:
1. Structural code-quality regressions
2. Missed opportunities for dramatic simplification
3. Spaghetti or branching complexity increases
4. Boundary, abstraction, or type-contract problems
5. File-size and decomposition concerns
6. Modularity and legibility concerns
Do not flag cosmetic nits when a larger structural issue is present. Prefer a smaller number of high-conviction findings over a long list of minor notes. Only report violations on changed lines with clear evidence from the diff.
## Require API Auth, Validation, and Tenant Scoping
Review API handlers and controller endpoints for missing authentication, authorization, tenant scoping, and request validation.
Flag new or modified endpoints that:
- Accept request bodies without runtime validation, such as Zod schemas, DTOs, or framework-native validators.
- Trust user-supplied identifiers for tenant, organization, or account access instead of deriving access from the authenticated session.
- Return sensitive internal errors or use inconsistent HTTP status codes for 400, 401, 403, 404, and 500 cases.
- Mix reads and writes in a way that makes side effects hard to reason about.
- Add public or high-volume endpoints without rate limiting or abuse controls.
## Enforce Pragmatic Test Coverage
Be reasonable when evaluating tests. Flag changes when important behavior is untested, but do not require tests for every export, type, config, metadata file, or mechanical change.
Flag PRs that:
- Change core business logic without covering the main success and failure paths.
- Add UI behavior without testing the user-visible interaction or state change.
- Add tests that only assert rendering, existence, snapshots, or that a function returns any value.
- Skip, mute, or weaken existing tests without a clear reason.
- Mock so much of the system that the test no longer validates the behavior the PR claims to change.
## Flag Security Vulnerabilities
Scan for security flaws: hardcoded secrets, SQL injection, XSS, weak auth, exposed data, poor crypto, unsafe deserialization, insecure HTTP calls in production, and more. Ensure all network requests use HTTPS/TLS. Identify attack vectors and security violations to maintain robust defenses.
## Flag AI Slop and Fabricated Changes
Catch AI-generated changes that look finished but aren't, such as placeholder or fabricated behavior, claims the code doesn't back up, and references that don't resolve.
Be conservative. Flag only obvious, evidence-backed low-quality AI output in the diff.
Flag changes that:
- Ship placeholder, mock, demo, fake-data, temporary debug, or filler text to production paths without a clear reason.
- Make claims in docs, comments, config, or PR text that contradict the implementation. Distinguish claims about current behavior or verification from documented plans or explicit statements that a capability is not supported.
- Reference files, commands, routes, functions, env vars, or config keys that do not resolve in their intended environment. Check the documented setup, including aliases, generated definitions, and externally supplied inputs.
- Add duplicated, patchwork, or overengineered code where a smaller cohesive solution would clearly work.
- Add narrating comments that restate obvious code instead of explaining intent or constraints.
- Change consequential behavior without meaningful verification, add tests that bypass the behavior they claim to cover, or weaken assertions to permit a regression. For coverage concerns, identify a realistic failure the verification would miss; consider existing shared tests and relevant type or static checks.
Do not flag:
- Generated files, fixtures, snapshots, migrations, vendored code, or mechanical renames unless one of the rules above clearly applies.
- Explicitly labeled scaffolding with concrete follow-up guidance, provided unfinished behavior is isolated from active production use. A TODO does not excuse fabricated results in a live path.
- Well-scoped AI-assisted code that is cohesive and tested.
## Prevent Sensitive Data in Logs
Logging, error reporting, analytics events, and audit trails must not expose sensitive data. Flag code that logs PII, emails, passwords, API keys, auth tokens, session identifiers, payment data, or raw request/response bodies without redaction. Error serialization must preserve enough context to debug while sanitizing secrets and user data.
Questions and answers
- What should a TypeScript code review checklist include?
- Start with the places where types are claimed rather than checked: type assertions with as, non-null assertions, and comments that silence the checker, such as @ts-expect-error or an eslint-disable line for no-explicit-any. Then check that data from JSON.parse, fetch responses, environment variables, and browser storage passes through a runtime schema before typed code uses it. Finally, look for switches over unions without a never check, state types built from booleans and optional fields, and catch blocks that hide failures or drop the original error.
- Is TypeScript strict mode enough to catch type errors?
- The strict flag enables checks such as strictNullChecks, noImplicitAny, and useUnknownInCatchVariables, but it still accepts assertions between overlapping types, double assertions through unknown, non-null assertions, and @ts-ignore comments, and it trusts the any value that JSON.parse returns. It also leaves out noUncheckedIndexedAccess, so items[0] and record[key] are typed as present even when they are missing. Types are erased at runtime, so any value that enters from outside the program needs a runtime check before it matches its declared type.
- How do I make AI coding tools follow these TypeScript standards?
- Paste the AGENTS.md export into AGENTS.md at the root of your repository. Codex, Cursor, and other AI coding tools read that file. Claude Code reads CLAUDE.md, and GitHub Copilot reads files in .github/instructions, so there is an export for each.
- How does cubic enforce these TypeScript standards?
- cubic runs each agent on every pull request, up to your plan’s per-repository agent limit, and leaves an inline comment, naming the agent, when a change breaks the standard. Add an agent from its page, or commit the cubic.yaml export to your repository.