TypeScript Code Reviews: What Experienced Teams Look For
AI generated
<T>
type
TypeScript · Code Review · Type Safety · Team Practice
TypeScript Code Reviews: What Experienced Teams Look For
Spotting red flags without slowing down velocity

A TypeScript review that only checks syntax misses the actual risks: any creep, unnecessary as assertions, unreadable generic constructs, and non-null assertions that mask real error sources. This article covers the most important red flags experienced reviewers look for, a practical checklist, and a way to keep type-safety rigor and review speed in balance.

11 min read any creep · as assertions · Generics ESLint · tsc --noEmit · Pull Requests

1. Why TypeScript reviews are more than syntax checks

A TypeScript code review that stops at formatting, naming conventions, and obvious bugs misses the actual value of the type system: TypeScript promises to eliminate entire classes of errors at compile time, but that promise only holds if the code actually uses the type system instead of working around it. Experienced reviewers therefore check not just whether the code compiles, but whether the types correctly model the real domain and whether type safety was deliberately or accidentally sacrificed at critical points.

The difference between a superficial and a thorough review usually comes down to the same recurring patterns: any in places that could actually be precisely typed, as assertions that silence the compiler instead of solving a real problem, and non-null assertions that simply define away a genuinely possible null situation. These patterns are not always wrong, but they deserve a deliberate question in review, not automatic approval.

2. any creep: the slow erosion of type safety

any creep describes how any spreads unnoticed through a codebase: a function gets an any parameter because an external type is not currently available, the return value of another function implicitly becomes any too, and every subsequent call silently loses its type checking. The tricky part: the compiler reports zero errors, because any is compatible with every other type. The code snippet looks finished but behaves like plain JavaScript with no safety net at all.

In review, it pays to actively search for any in function signatures, in return types, and in assertion chains like (value as any) as SpecificType, a clear signal that the actual type error was worked around instead of solved. unknown is the safer alternative in almost every case where the concrete type is not known at compile time, because it forces the caller into an explicit type check before the value can be used. An enabled noImplicitAny in tsconfig.json at least prevents the unintentional cases.


// RED FLAG: any spreads silently through the call chain
function parseApiResponse(raw: any) {
  return raw.data.items; // no error, even if "data" does not exist
}

function loadProducts(json: string) {
  const parsed = JSON.parse(json); // implicitly "any"
  return parseApiResponse(parsed); // type safety already lost here
}

// BETTER: unknown forces an explicit check before use
interface ApiResponse {
  data: { items: Product[] };
}

function isApiResponse(value: unknown): value is ApiResponse {
  return (
    typeof value === "object" &&
    value !== null &&
    "data" in value
  );
}

function parseApiResponseSafe(raw: unknown): Product[] {
  if (!isApiResponse(raw)) {
    throw new Error("Unexpected API response shape");
  }
  return raw.data.items; // fully typed from here on
}

3. Unnecessary type assertions with as

An as assertion tells the compiler: "trust me, I know better." That is sometimes justified, for example when working with DOM APIs that TypeScript cannot type precisely enough, but in many pull requests as is instead used to silence a genuine type error that should actually be fixed. A reviewer's reflex for every assertion should be: could this also be solved without as, for example through a correct type definition, a type guard, or a small refactor of the data structure?

Especially risky are assertions that narrow a type without any runtime check happening, for example const user = data as User with no prior validation. If data does not actually match the shape of User at runtime, the code produces no compile error, but a runtime error at a completely different location, often several function calls later. A review comment like "how do we know this holds true at runtime?" reliably surfaces these cases and usually leads to a more robust type guard instead of an assertion.


// RED FLAG: assertion without runtime validation
function getUserFromCache(key: string): User {
  const raw = cache.get(key);
  return raw as User; // compiles fine, may explode later at runtime
}

// BETTER: validate before narrowing, throw with context if invalid
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value
  );
}

function getUserFromCacheSafe(key: string): User {
  const raw = cache.get(key);
  if (!isUser(raw)) {
    throw new Error(`Cache entry for "${key}" is not a valid User`);
  }
  return raw; // narrowed by the type guard, no assertion needed
}

4. Generic gymnastics: when types hurt readability

Generics are a powerful tool, but a review red flag appears as soon as a type parameter construct takes longer to understand than the actual function logic. Nested conditional types, several nested infer clauses, or generics with five or more type parameters are typical signs that the abstraction has outgrown its practical value. The test experienced reviewers apply: can a new team member understand the signature in under a minute, without reading the implementation code?

Often the same type safety can be achieved with a simpler structure, for example by splitting an overly generic function into two more specific variants, using named helper types instead of inline constructs, or dropping a type parameter that only ever takes a single concrete type across the entire project. Complex generics, if truly needed, belong in well-documented, central utility types, not scattered across individual feature modules where they have to be re-decoded at every review.


// RED FLAG: deeply nested conditional types, hard to read at a glance
type DeepPartialExtract<T, K> = T extends object
  ? K extends keyof T
    ? { [P in K]: T[P] extends Array<infer U> ? DeepPartialExtract<U, keyof U>[] : T[P] }
    : never
  : never;

// BETTER: split into named, documented steps
type ArrayElement<T> = T extends Array<infer U> ? U : never;

/** Extracts a single top-level property while keeping array element types intact. */
type ExtractField<T, K extends keyof T> = {
  [P in K]: T[P] extends unknown[] ? ArrayElement<T[P]>[] : T[P];
};

// Usage stays simple and self-explanatory for reviewers and newcomers
type ProductSummary = ExtractField<Product, "id" | "variants">;

5. Non-null assertions and masked null checks

The exclamation mark in value!.property tells the compiler that value is guaranteed not to be null or undefined at runtime. In many codebases, this non-null assertion is used in places where that guarantee does not actually hold, usually because an error message at that point would be inconvenient. The result is a classic silent runtime failure: Cannot read properties of undefined, at a spot where TypeScript would have warned the developer, had the assertion not been there.

A particularly common pattern in React or Alpine.js-adjacent code: document.getElementById("cart")!.classList.add("open"), even though the element can genuinely be missing depending on page state. Reviewers should ask, for every non-null assertion, whether the case is truly impossible or merely unlikely, and in the latter case insist on an explicit guard with a meaningful error message. Optional chaining ?. combined with the nullish coalescing operator ?? solves most of these cases without a single assertion.


// RED FLAG: non-null assertion masks a genuinely possible null case
function openCart() {
  document.getElementById("cart")!.classList.add("open"); // silent crash risk
}

function getFirstVariant(product: Product) {
  return product.variants![0].sku; // assumes variants is never empty
}

// BETTER: explicit guard with a meaningful error, or safe fallback
function openCartSafe() {
  const cartElement = document.getElementById("cart");
  if (!cartElement) {
    console.warn("Cart element not found, skipping open()");
    return;
  }
  cartElement.classList.add("open");
}

function getFirstVariantSafe(product: Product): string | undefined {
  return product.variants?.[0]?.sku ?? undefined;
}

6. A practical review checklist for TypeScript PRs

A written checklist makes reviews more consistent, since it covers the same critical points regardless of a reviewer's mood on a given day. A short list that belongs directly in the PR template or team wiki works best: does the diff contain new any occurrences without a comment justifying them? Are new as assertions backed by a comment or a preceding type guard? Does a generic construct exceed two type parameters without a comment explaining the purpose? Are there non-null assertions in places that are not obviously impossible?

API boundaries deserve their own spot on the list too: is external data, for example from a fetch call or a form input, actually validated before use, or is a type blindly assigned to it? And finally, the consistency question: does the new code follow the same type patterns as the rest of the codebase, for example using satisfies for configuration objects or a consistent error type structure? A checklist like this should stay short, at most six to eight points, or it gets ignored in daily practice.

7. Automation: ESLint, tsc --noEmit, and CI gates

Many of the red flags mentioned can be caught automatically before a human ever looks at the diff. @typescript-eslint/no-explicit-any flags new any occurrences, @typescript-eslint/no-non-null-assertion forces a deliberate exception rule for every !, and @typescript-eslint/consistent-type-assertions restricts where as is allowed at all. Enforcing these rules as a CI gate, instead of only showing them as an editor warning, prevents red flags from ever becoming a review discussion in the first place.

A tsc --noEmit step in the pipeline ensures that no PR gets merged that introduces new type errors, regardless of whether a human reviewer would have noticed them. For rules that cannot be cleanly automated, such as the readability of complex generics, human review remains irreplaceable, but automation noticeably reduces the amount of mechanical checking a human still has to do.


# package.json script section: fail fast, cheapest checks first
# "typecheck": "tsc --noEmit",
# "lint": "eslint . --max-warnings=0",

# CI pipeline step: block merge on type errors or lint violations
npm run typecheck && npm run lint

# Local pre-commit hook via lint-staged, catches issues before push
npx lint-staged --config '{
  "*.ts,*.tsx": ["eslint --fix", "tsc-files --noEmit"]
}'

8. Review communication: criticize the code, not the person

The most technically correct checklist is of little use if review comments come across as personal attacks and developers start reaching for assertions and any to avoid a discussion, instead of solving a real problem. Phrasing like "this assertion could fail at runtime here, should we add a guard instead?" instead of "this is wrong" opens a discussion instead of closing it, and experience shows it leads to better, not just more compliant, solutions.

At the same time, a reviewer should not turn every stylistic preference into a blocker. A red flag like unvalidated any at an API boundary justifies a "request changes," while an unusual but correct generic formulation is better left as an optional comment. This grading, applied consistently, is the actual difference between a review process that ensures quality and one that only creates friction.

Mironsoft

TypeScript reviews, type-safety audits, and CI gates for frontend teams

Ready to level up your TypeScript reviews?

We analyze your existing codebase for any creep, risky assertions, and overcomplex generics, set up matching ESLint rules and CI gates, and train your team on practical TypeScript review.

Type-safety audit

Systematically track down any creep, unsafe assertions, and gaps

ESLint and CI setup

Configure rules and gates that match your team's pace

Review coaching

Checklists and communication patterns for your dev team

9. Red flags compared side by side

The overview below summarizes the most important TypeScript review red flags: the problematic pattern, why it is risky, and the preferred alternative a review should push toward.

Red flag Risk Preferred alternative
any parameter Type checking drops for the whole call chain unknown with a type guard
as without validation Runtime error instead of a compile error Type guard before narrowing
Nested conditional types Signature not understandable in under a minute Named, documented helper types
Non-null assertion (!) "Cannot read properties of undefined" Optional chaining plus a guard
Unvalidated API response Wrong assumption about external data shape Schema validation at the boundary

No single pattern in this table is forbidden by itself, each has legitimate exceptions. What matters in review is not automatic rejection, but a deliberate question: is this spot really an exception, or was a shortcut taken here that will later turn into a real bug?

10. Summary

Good TypeScript code reviews go beyond formatting and obvious bugs, checking deliberately whether the type system is actually being used or merely worked around. any creep, unnecessary as assertions, overcomplex generics, and non-null assertions in unsafe places are the recurring patterns experienced reviewers reliably spot and question, instead of waving them through automatically.

A short, written checklist keeps different reviewers consistent, while ESLint rules and a tsc --noEmit gate in CI already catch many cases automatically before a human even needs to review. The decisive success factor, though, remains communication: reviews that name problems instead of criticizing people lead to more robust code, without slowing down team velocity through endless discussions over every stylistic detail.

TypeScript Code Reviews - The essentials at a glance

Stop any creep

Use unknown instead of any at API boundaries, enable noImplicitAny in tsconfig.

Question assertions

Every as and every ! deserves the question of whether a runtime check is missing.

Simplify generics

Signatures must be understandable in under a minute, otherwise split them up.

Automate what you can

ESLint rules and tsc --noEmit as a CI gate take load off the human review.

11. FAQ: TypeScript Code Reviews

1What is any creep and why is it dangerous?
any spreads unnoticed through a codebase, because functions with any parameters implicitly make their return types any too, without the compiler reporting an error.
2When is a type assertion with as justified?
When TypeScript cannot infer a type precisely enough but correctness is otherwise guaranteed. For unvalidated external data, a type guard is preferable.
3How do I spot overly complex generics in review?
Test: understandable in under a minute without reading the implementation? Nested infer clauses or many type parameters usually signal too much abstraction.
4Why are non-null assertions risky?
They suppress a genuine compiler warning with no runtime guarantee. If the assumption fails, a runtime error results, often far from the actual cause.
5What belongs on a good TypeScript review checklist?
New any occurrences, unjustified as assertions, risky non-null assertions, unvalidated external data, and consistency with existing type patterns. Keep it short, six to eight points max.
6How do I automate detecting any and assertions?
ESLint rules like no-explicit-any, no-non-null-assertion, and consistent-type-assertions from @typescript-eslint, configured as a CI gate.
7How much time should a TypeScript review cost per PR?
No fixed number, but automated CI checks should cover the mechanical part, freeing the human reviewer to focus on domain logic and complex type constructs.
8How do I avoid reviews becoming pure friction?
Clearly separate blocking red flags from optional style questions. Only genuine risks justify a request changes.
9Is unknown always better than any?
For most cases yes, since unknown forces an explicit check. In rare cases with deliberately dynamic code, any with an explanatory comment can make more sense.
10How do you constructively address red flags in a review comment?
Phrase it as a question and suggest a concrete alternative like a type guard. This opens a discussion instead of coming across as personal criticism.