Let’s be honest: a lot of code reviews are performative. We check for obvious bugs, suggest a few variable renames, and nitpick code style that a linter should’ve caught. We click “Approve,” collect our team-player points, and move on. For general Best Practices for Code Review in JavaScript, there are other great resources. But a good **TypeScript Code Review** is a totally different beast. It’s less about ticking boxes and more about collaborative architecture.
When you’re reviewing TypeScript, you’re not just looking at code; you’re looking at the *shape* of the data, the *contracts* between components, and the *story* the types are telling. Get it right, and you prevent entire classes of bugs. Get it wrong, and you’re just writing JavaScript with extra steps.
This isn’t about dogma. It’s about making our lives easier, building things that don’t break in weird ways, and leaving the codebase better than we found it.
Beyond “Looks Good to Me”
A great TypeScript review goes deeper than surface-level correctness. It’s a force multiplier for quality, acting as a key part of Improving Code Quality: A Developer’s Guide.
First, it’s our best defense against type-related bugs. Think about that one time a function expected a `string` but got a `number` at runtime because an API response changed. A proper type definition, maybe with a validation library like Zod, would have caught that before it ever hit production.
Second, it drastically improves readability. Good types are a form of documentation that never goes out of date. When you see function processUser(user: User)
, you immediately know what kind of data structure to expect. No more digging through three files to figure out what a `user` object looks like.
But here’s the part that senior devs really care about: it forces better API design. When you’re reviewing a PR, you’re the first “customer” of that new function, component, or class. If you can’t figure out how to use it from its type signature alone, the API is too complicated. The review becomes a design session.
What to Actually Look For: The Core of a Great TypeScript Code Review
Okay, so where do you focus your attention? When considering What to Prioritize in a Code Review, it comes down to a few key areas.
Type Safety and Correctness
This is the starting point. If the types are wrong, nothing else matters.
This content is solid — clear opinion, technical depth, and great examples. Structure’s already good. Below is a slightly refined version to improve flow, consistency, and rhythm (especially in titles and pacing):
🔓 The any
Loophole
My take?
any
is a code smell — not a cardinal sin. It basically says:
“I’m giving up on type safety here.”
Every use of any
should be questioned:
- Is it temporary?
- Did it come from a third-party lib with no types?
- Could you use
unknown
instead?
unknown
is the safer cousin of any
: it forces you to validate the type before using the variable — which is almost always the right thing to do.
🚨 Abusing Type Assertions
Seeing as User
or <User>
is a red flag.
It’s like telling TypeScript:
“Trust me, I know what I’m doing.”
Sometimes you do — like:
document.getElementById('app') as HTMLDivElement
But most of the time, type assertions are used to cover up a type mismatch that should be fixed properly.
Use type guards instead:
function isUser(obj: unknown): obj is User
It’s more robust, more explicit, and way safer.
✅ Dealing with null
and undefined
If your project still doesn’t use "strictNullChecks": true
in tsconfig.json
, stop what you’re doing and talk to the team about enabling it.
It hurts a bit at first — but it eliminates errors like:
"Cannot read property 'x' of undefined"
In code review, watch out for overly defensive code:
if (user && user.profile && user.profile.name)
When it could just be:
user?.profile?.name
🎯 API Design and Type Definitions
This is where you wear your product designer hat.
How does using this code feel?
Type names that tell a story
ItemData
is a lazy name.
What kind of item? ProductListItem
? ShoppingCartItem
?
Be specific — good names make code self-explanatory.
For functions (especially in shared monorepo packages), explicit return types are a must.
Don’t make the consumer guess what your function returns.
Clean, consistent interfaces
Look at the props
of a React component.
Is it just a bunch of booleans?
<Button isPrimary isDestructive isIconOnly />
That’s a classic sign that a union type would be better:
<Button variant="primary" | "destructive" | "icon" />
This prevents impossible states — you don’t want a button to be both “primary” and “destructive”.
Generics for reuse
If you see similar functions that only differ in the types they handle, that’s a perfect use case for generics.
Example:
fetchUserData()
fetchProductData()
Could become:
fetchData<T>(url: string): Promise<T>
Spotting these during a review is a big win for maintainability.
🛑 Error Handling
Errors will happen. How does your type system deal with them?
The key point here is: how do you safely catch them?
Since TypeScript 4.4, the default type in catch
blocks is unknown
(it used to be any
). That’s great for safety — but only if the team uses it correctly.
Problematic pattern (still common in older configs):
try {
// ...
} catch (err) {
console.error(err.message); // err is `any`
}
The ideal is always to be explicit and safe:
try {
// ...
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.message);
} else {
console.error("An unknown error occurred");
}
}
Reviewers should suggest this pattern whenever they see a vague catch
.
It ensures you don’t try to access properties on something that might be a string, a number, or even null
.
How to Run the Review Process
The process matters as much as the content. While this guide focuses on human interaction, for insights into AI Code Reviews vs. Traditional Code Reviews, you might explore other discussions.
For the Author: Set Yourself Up for Success
Don’t just toss your code over the wall. A bit of prep goes a long way.
Do a self-review first:
Run tsc --noEmit
— it’s your first line of defense. It catches type errors without having to build the whole project.
Run your linter and formatter: eslint . --fix
and prettier --write .
Fix what’s automatic. That frees up the reviewer to focus on what actually matters.
Want an extra hand? You can also use AI tools to help review your code.
Write a solid PR description: Explain the why, not just the what.
If you had to write a really complex mapped or conditional type, add a comment or link to the TypeScript doc you used. Give the reviewer some context!
For the Reviewer: Be a Mentor, Not a Cop
Your goal is to improve the code — not prove you’re smarter.
Understand the intent: Read the PR description and the linked ticket. What problem is this change solving? That context helps you avoid off-target suggestions.
Prioritize your feedback:
Use prefixes in your comments. Something simple works great:
[Blocker]
: “This looks like a bug or a significant architecture issue. We need to fix it before merging.”[Suggestion]
: “I think this could be clearer/more efficient by doing X. What do you think?”[Nitpick]
: “Just a style thing — feel free to ignore.”
Offer solutions: Don’t just say “this is confusing.” Say: “This conditional logic is a bit hard to follow. How about extracting it to a function called isEligibleForDiscount(cart)
? It would make the intent clearer.”
TypeScript Code Review Checklist
Here’s a practical, no-fluff checklist to run through on your next PR. Think of it as a guide, not a rigid set of rules.
✅ Typing & Correctness
- Is
any
used? If so, is it justified? Couldunknown
be a safer choice? - Are type assertions (
as Foo
) used? Could a type guard or better type eliminate the need? - Are function return types explicit, especially for exported functions?
- Are generics used where logic is reusable?
- Are
null
andundefined
handled properly? Is optional chaining (?.
) and nullish coalescing (??
) used correctly? - Are union types (
|
) used to represent valid states instead of multiple booleans?
✅ Organization & Maintainability
- Are type names descriptive and meaningful? (
Product
vsData
) - Are types co-located with the code that uses them, or placed in a clearly scoped
types.ts
? - In monorepos, are shared types exported cleanly with clear boundaries?
- Do complex types have a named
type
orinterface
to promote reuse and clarity?
✅ Testing
- Is complex type logic (e.g., conditional types) tested with
tsd
,expect-type
, or similar tools? - Are mocks and test data correctly typed and representative of real data structures?
✅ Security
- Is external data (API responses, user input) validated before being trusted? Is it typed as
unknown
initially and narrowed later? - Are sensitive strings (e.g., passwords, tokens) branded or typed to prevent accidental logging?
type Password = string & { __brand: 'password' }
✅ Frontend (React)
- Are component props clearly typed? Are optional props marked with
?
? - Are event handlers correctly typed (e.g.,
React.MouseEvent<HTMLButtonElement>
) instead ofany
? - When using
useState
, is the initial state typed correctly? If nullable, is ituseState<Type | null>(null)
? - Do custom hooks return a clear, well-typed tuple or object (e.g.,
[string, (value: string) => void]
)?
✅ Backend (Node.js)
- Are API request bodies and query parameters validated and typed (e.g., using
Zod
,Joi
, etc.)? - Are database models/schemas strongly typed and reflective of the actual DB schema?
- Are environment variables parsed and typed at startup, rather than left as
string | undefined
?
By moving beyond a simple “LGTM” and engaging with the code on this level, you’re doing more than just reviewing code. You’re shaping a more stable, maintainable, and developer-friendly product, contributing to how to build a strong code review culture within your team. And that’s a review worth doing.