TypeScript Code Review Checklist | Practical Guide

A TypeScript code review needs to go beyond syntax errors, variable names, or details the linter should already catch.

When reviewing a TypeScript PR, it is not enough to check whether the code compiles. What matters most is understanding whether the types accurately represent the data and whether the contracts created by the code are safe to use.

This is where many problems tend to go unnoticed.

TypeScript helps reduce errors, but it does not prevent bad typing decisions. Code can be perfectly valid to the compiler and still hide fragile contracts, incorrect assumptions, or unexpected runtime behavior.

This guide provides a practical checklist for reviewing TypeScript code with more attention to the issues that actually matter and can eventually turn into production bugs.

For a broader look at JavaScript reviews, see our guide to JavaScript code review best practices.

What should you review in a TypeScript PR?

In a TypeScript PR, a few areas deserve special attention: type safety, the use of any, type assertions, handling of null and undefined, public contracts, and validation of external data.

Depending on the project, this can also include React props, Node.js application inputs, and the data used in tests.

A good place to start is this checklist:

  • Is the use of any justified?
  • Should the value start as unknown?
  • Is an as Type assertion hiding a real incompatibility?
  • Do exported functions have clear contracts?
  • Are nullable values handled correctly?
  • Do the types allow states that should not exist?
  • Is external data validated before being used?
  • Are React props and events typed correctly?
  • Are environment variables validated at startup?
  • Do tests use data that matches the real types?

These checks already cover a large share of TypeScript-specific issues that tend to slip through code review.

Why does TypeScript code review need its own checklist?

TypeScript changes some of the things you need to look for during a review.

The compiler can already identify many problems before the code reaches a PR. What it cannot tell you is whether the types actually represent the system’s behavior.

Consider this code:

const user = response.data as User;

As far as TypeScript is concerned, user is now a User. But no data has actually been validated.

If the API returns something different from the expected contract, the problem still exists even if the code compiles without errors.

That is why a review needs to go beyond asking, β€œIs this typed?” and instead ask whether that type can actually be guaranteed at that point in the code.

How should you review the use of any in TypeScript?

any deserves attention because it removes many of the guarantees TypeScript is supposed to provide in that part of the code.

That does not mean every use of any is necessarily a problem. There are cases where it is unavoidable or temporarily acceptable. What matters is understanding why it is being used.

During the review, check whether:

  • there is a real limitation in a library or integration
  • the usage is temporary and well isolated
  • unknown would be a safer alternative
  • there is a more specific type that could be used

The main risk is allowing any to spread through the codebase. From that point on, properties can be accessed and values can be passed around without the compiler being able to verify much.

When the shape of a value is still unknown, especially with external data, unknown usually represents that situation more accurately.

When should you use unknown instead of any?

Use unknown when the code receives a value whose shape has not been verified yet.

This often comes up with API responses, requests, webhooks, queue messages, files, or any other external input.

The important difference is that unknown forces the code to validate or narrow the value before using it.

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value
  );
}

This makes the boundary between unknown data and a value the rest of the application can safely trust explicit.

During the review, pay particular attention to places where external data enters the system and is immediately treated as an internal type.

How should you review type assertions in TypeScript?

Type assertions such as as User do not validate or transform a value. They only tell the compiler which type to assume.

That is why they deserve special attention during review.

There are situations where an assertion makes sense, especially when the code genuinely knows something TypeScript cannot infer. The problem is when it becomes a quick way to work around a type error.

const user = response.data as User;

If response.data comes from an external source, as User does not guarantee that the value actually has that shape.

In those cases, prefer real validation:

const user = UserSchema.parse(response.data);

Or an equivalent type guard.

During review, try to understand whether the assertion is documenting a real guarantee in the code or simply hiding uncertainty.

How should you review null and undefined in TypeScript?

TypeScript projects benefit significantly from strictNullChecks because it forces the code to handle null and undefined explicitly.

During review, the important part is checking whether the absence of a value is actually part of the contract and whether it is handled consistently.

Optional chaining can simplify safe property access:

return user?.profile?.name;

And ?? is usually the right choice when a fallback should only apply to null or undefined:

const displayName = user.profile?.name ?? "Anonymous";

It is also worth watching for uses of || with values where 0, false, or an empty string are valid.

The main question is whether the code distinguishes between a missing value and a valid value that simply happens to be falsy.

How should you review API types in TypeScript?

A well-typed API should make it clear how it can be used and reduce the number of invalid states or calls that are possible.

When reviewing an exported function, hook, component, or shared package, it is worth looking at the signature first.

export function buildReviewSummary(
  pullRequest: PullRequest,
  comments: ReviewComment[]
): ReviewSummary {
  // ...
}

It shows what goes in, what comes out, and which contract other parts of the application will depend on.

Explicit return types can be especially useful for public or shared APIs because they prevent internal changes from unintentionally changing the contract.

It is also worth checking whether type names accurately represent the domain. Generic names such as Data, Item, Info, or Payload tend to lose meaning as the project grows.

The goal is not to make every type more verbose. It is to make the contract understandable to whoever needs to use it later.

How should you review React props with TypeScript?

In React, the main concern is not simply whether props have types, but whether those types accurately represent the states the component can be in.

A common warning sign is a component with several related boolean props:

<Button isPrimary isDestructive isIconOnly />

Depending on the expected behavior, those props may allow combinations that do not make sense.

In some cases, a union makes the contract clearer:

type ButtonVariant = "primary" | "destructive" | "icon";

type ButtonProps = {
  variant: ButtonVariant;
  children?: React.ReactNode;
};

During review, it is also worth checking for:

  • optional props that should actually be required
  • callbacks typed as Function
  • handlers that have fallen back to any
  • nullable state without a clear type
  • hooks that return structures that are difficult to understand

The most useful question here is: does the type help prevent invalid states, or does it simply describe every possible combination?

How should you review error handling in TypeScript?

Values caught in a catch block should not automatically be treated as Error.

In JavaScript, any value can be thrown. That means the code needs to narrow the value before accessing specific properties.

try {
  await runImport();
} catch (err: unknown) {
  if (err instanceof Error) {
    console.error(err.message);
  } else {
    console.error("Unknown import failure");
  }
}

This matters especially in code that logs errors, creates user-facing messages, or sends information to observability tools.

During review, check that error handling preserves the original problem instead of creating a new failure while trying to inspect it.

How should you review Node.js code in TypeScript?

On the backend, many problems show up at the boundaries of the system.

Internal code can be well typed, but requests, webhooks, queues, environment variables, and external services are still runtime data.

That is why you should review these areas carefully:

  • request bodies
  • query and route params
  • headers
  • webhooks
  • queue messages
  • environment variables
  • responses from external services

Avoid relying only on assertions such as:

const input = req.body as CreateUserInput;

When possible, validate the input before using it:

const input = CreateUserSchema.parse(req.body);

The same applies to configuration. If an environment variable is required, it is better to discover that during application startup than only when a particular route is executed.

How should you review generics in TypeScript?

Generics are useful when there is an abstraction that genuinely needs to work with different types.

The problem appears when a generic creates a guarantee that the runtime cannot actually enforce.

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

In this case, T has not been validated. The caller chooses the type, but nothing guarantees that the response actually has that shape.

One alternative is to make the function receive the validation logic:

async function fetchData<T>(
  url: string,
  parse: (value: unknown) => T
): Promise<T> {
  const response = await fetch(url);
  const data: unknown = await response.json();

  return parse(data);
}

During review, look at both extremes: duplicated code that could benefit from an abstraction, and generics that are too complex for a simple problem.

The abstraction should reduce complexity, not just move it somewhere else.

How should you review tests in TypeScript projects?

The types used in tests also need to represent the reality of the code.

One pattern worth watching for is using assertions just to create incomplete fixtures:

const user = {
  id: "1"
} as User;

This allows the test to ignore fields that the real code considers required.

Builders and factories are usually a better alternative because they keep test data aligned with the application’s contracts.

function createUser(overrides: Partial<User> = {}): User {
  return {
    id: "user-1",
    email: "user@example.com",
    name: "Test User",
    ...overrides
  };
}

It is also worth checking whether invalid inputs are covered by tests when the code handles external data.

TypeScript helps guarantee what happens inside the system. It does not prevent an API, webhook, or user from sending an unexpected value.

Complete TypeScript code review checklist

Not every item will be relevant to every PR, but this checklist can serve as a reference during review.

Types and correctness

  • Was any used? Is there a clear reason?
  • Would unknown be more appropriate for data that has not been validated yet?
  • Are type assertions hiding incompatibilities?
  • Are parsers or type guards used at the right boundaries?
  • Do public functions have clear contracts?
  • Are nullable values handled correctly?
  • Would ?? be more appropriate than ||?
  • Do the types prevent invalid states?
  • Do the types represent the real data?

APIs and contracts

  • Does the signature make it clear how the function should be used?
  • Do type names accurately represent the domain?
  • Do shared types have clear boundaries?
  • Could internal changes unintentionally alter public contracts?
  • Does the API allow invalid combinations or states?
  • Is external data validated before entering the domain?

React

  • Are props typed correctly?
  • Are optional props actually optional?
  • Do event handlers avoid any?
  • Does nullable state have an explicit type when needed?
  • Do hooks return clear structures?
  • Does the type model prevent invalid combinations?

Node.js and backend

  • Is the request body validated before use?
  • Are params converted and validated?
  • Are environment variables checked at startup?
  • Are webhooks and queues treated as external data?
  • Do database types match the real schema?
  • Are errors narrowed before their properties are accessed?

Tests

  • Do fixtures respect the real types?
  • Are assertions being used to hide incomplete data?
  • Are invalid inputs covered?
  • Do mocks represent contracts that are close to the real ones?
  • Do more complex types need dedicated type-level tests?

How should you give feedback on a TypeScript PR?

A good review comment explains the risk instead of simply pointing out that something looks wrong.

Instead of:

This is confusing.

A more useful comment would be:

This casts response.data as User, but the value comes from an external API.
Can we parse it before returning from the API client?

In this case, the author understands why it matters and what change the reviewer is suggesting.

It can also help to separate merge-blocking comments from smaller suggestions, as long as the team has a clear convention for doing so.

The goal is not to classify everything. It is to avoid making a style detail look as important as a bug or an unsafe contract.

How should you prepare a TypeScript PR for review?

Before requesting a review, run the automated checks that are already part of the project.

For example:

tsc --noEmit
eslint .
prettier --check .

The commands may vary between repositories. What matters is not leaving reviewers to catch problems that the compiler, linter, or CI can already find automatically.

The PR description also helps a lot.

If the change modifies a public contract, adds important validation, or changes how some data is represented, make that clear.

A good description helps the reviewer understand:

  • what problem is being solved
  • which contracts changed
  • which inputs or integrations were affected
  • where they should pay extra attention during the review

The more context there is before opening the diff, the less time the reviewer needs to spend reconstructing the intent behind the change.

A good TypeScript review reduces surprises in production

The main role of TypeScript is to make contracts explicit. The role of code review is to verify that those contracts actually make sense.

Do the types represent the real data? Does the code prevent invalid states? Is external input validated before being treated as trusted?

When a review focuses on those questions, it catches a class of problems the compiler cannot solve on its own.

A good TypeScript code review is not just looking for code that passes tsc. It is looking for code whose types remain true when the application meets the real world.