The any type bypasses type checking, letting bugs slip through. Runtime errors
occur because types claim one shape but data has another. Undefined properties
crash production because optional chaining wasn't used. These issues pass the
compiler and often escape review because TypeScript's flexibility allows unsafe
patterns.
This checklist helps catch what matters: type safety violations, any type
abuse, incorrect type assertions, and code that fights TypeScript instead of
leveraging its type system effectively.
Quick Reference Checklist
Use this checklist as a quick reference during code review. Each item links to detailed explanations below.
Type Safety
anytype avoided except when truly necessary (external APIs, migrations)unknownused instead ofanyfor unknown types- Type assertions justified and safe:
value as Typenotvalue as any as Type - No type assertions from
any: shows type wasn't understood - Function return types explicitly declared
- Object types use
interfaceortype, not inline types repeated - Generic constraints specify requirements:
<T extends User>not just<T> - Discriminated unions for polymorphic types instead of type assertions
Strict Mode Configuration
strict: trueenabled intsconfig.jsonstrictNullCheckscatches undefined/null accessstrictFunctionTypesprevents unsound function assignmentsnoImplicitAnyprevents accidentalanytypesnoImplicitReturnsensures all code paths return valuesnoUncheckedIndexedAccesstreats indexed access as possibly undefinedstrictPropertyInitializationrequires class properties be initialized
Null Safety
- Optional chaining used for potentially undefined:
user?.name - Nullish coalescing provides defaults:
value ?? defaultValue - Type narrowing with
if (value !== undefined)guards - Non-null assertion
!only when truly guaranteed non-null - Undefined checks before accessing properties
- Function parameters optional when they might not be provided:
name?: string - Return types reflect possibility of undefined:
findUser(): User | undefined
Modern TypeScript Features
- Utility types used effectively:
Partial<T>,Pick<T, K>,Omit<T, K> - Const assertions preserve literal types:
as const - Template literal types for string unions:
type Color = `#${string}` - Conditional types for type transformations:
T extends U ? X : Y - Mapped types transform existing types:
{ [K in keyof T]: T[K][] } - Indexed access types extract property types:
User['email'] satisfiesoperator validates without widening type:config satisfies Config- Type predicates for custom type guards:
function isString(x): x is string
React & Frontend Integration
- Component props use
interfacewith meaningful names - Event handlers typed correctly:
React.ChangeEvent<HTMLInputElement> - Hook dependencies correctly typed
- Generic components specify type parameters:
List<User> - Children prop typed:
children: React.ReactNode - Refs typed appropriately:
useRef<HTMLDivElement>(null) - Context values fully typed, not
any - Custom hooks return tuples use
as constfor correct inference
Code Organization
- Types exported for public APIs
- Shared types in separate files, not duplicated
- Type aliases for complex types improve readability
- Namespace organization follows module structure
- Declaration files (
.d.ts) for JavaScript libraries - Type-only imports:
import type { User } from './types' - Enums used sparingly (prefer string literal unions)
- Branded types for nominal typing when needed:
type UserId = string & { __brand: 'UserId' }
Type Safety Fundamentals
The any type turns off type checking, defeating TypeScript's purpose. When we
see any in code review, we question whether it's truly necessary. Valid uses
include gradual migration from JavaScript, integrating with badly-typed
third-party libraries, or genuinely dynamic data where the structure can't be
known at compile time. These cases should be rare and documented. Most any
usage indicates the developer gave up on typing rather than solving a legitimate
problem.
unknown provides a safer alternative for unknown types. While any allows any
operation, unknown requires type checking before use. Code can assign any
value to unknown, but must narrow the type before accessing properties or
calling methods. The pattern
if (typeof value === 'string') { console.log(value.toUpperCase()) }
demonstrates type narrowing that makes unknown safe. This forces developers to
think about what types they're actually handling.
Type assertions claim knowledge the type checker can't verify. The syntax
value as Type tells TypeScript "trust me, this is actually this type." This
should be rare and justified. When we see type assertions, we verify the claim
is actually true. Double assertions like value as any as Type indicate the
types are incompatible and something is wrong. This pattern almost always hides
a real type error that should be fixed properly.
Generic constraints make type parameters useful by specifying requirements. An
unconstrained generic <T> means we can't safely do anything with values of
type T because we don't know what properties or methods exist. Constraints
like <T extends { id: string }> let us safely access the id property. When
reviewing generic code, we verify constraints capture actual requirements so the
generic code can be type-safe.
Discriminated unions provide type-safe handling of polymorphic data. The pattern uses a common property to distinguish variants:
type Result<T> = { success: true; value: T } | { success: false; error: string }
Checking the discriminant property narrows the type, making other properties safely accessible. This pattern eliminates need for type assertions and makes error handling explicit.
Strict Mode and Compiler Configuration
Strict mode represents TypeScript's best type checking. strict: true in
tsconfig.json enables multiple strict checks that catch common bugs. Projects
that disable strict mode or individual strict flags usually do so because
existing code doesn't pass stricter checking, not because strict checking is
wrong. When we see relaxed type checking, we consider whether gradual migration
to strict mode would improve code quality.
strictNullChecks treats null and undefined as distinct types rather than
assignable to everything. This catches the common bug of accessing properties on
potentially null or undefined values. Without this check, code like user.name
compiles even when user might be null, causing runtime errors. With strict
null checks, we must handle the possibility: user?.name or
if (user) { user.name }. This enforcement prevents a huge class of bugs.
noImplicitAny prevents accidentally creating any types by requiring explicit
type annotations when TypeScript can't infer a type. Function parameters are
common sources of implicit any. The code function process(data) creates an
implicit any for data. With noImplicitAny, we must write
function process(data: unknown) or provide a more specific type. This makes
types explicit and prevents accidental loss of type safety.
strictFunctionTypes enables sound checking of function parameter types,
preventing assignments that violate type safety. This catches subtle issues in
callback functions where parameter types might not match expectations. Without
this check, certain unsafe function assignments compile. The stricter checking
prevents these problems at compile time.
noImplicitReturns ensures all code paths in a function return a value when the
function declares a return type. Inconsistent returns create bugs where some
paths return the expected type and others return undefined implicitly. This
check forces complete implementation of return logic, catching missing returns
in conditional branches.
Null Safety and Optional Chaining
Optional chaining ?. safely accesses properties that might not exist. The
expression user?.profile?.email returns undefined if any step is null or
undefined, preventing the TypeError that direct property access would throw.
This should be our default for any property access where the object might not
exist. When we see code checking for existence before each property access,
optional chaining simplifies the logic.
Nullish coalescing ?? provides defaults only for null or undefined, unlike
logical OR || which also treats 0, "", and false as falsy. The
expression count ?? 0 uses 0 when count is null or undefined but preserves 0
if that's the actual count value. This distinction matters for values where
falsy values are valid. When we see || defaultValue in code, we consider
whether ?? defaultValue expresses the intent more precisely.
Type narrowing through control flow makes TypeScript understand that we've
checked for undefined. The pattern if (user !== undefined) { user.name } works
because TypeScript tracks that user is defined within the if block. Similarly,
early returns narrow types: if (!user) return; user.name knows user is defined
after the guard. These patterns communicate intent while satisfying the type
checker.
Non-null assertion ! claims a value isn't null or undefined: user!.name.
This bypasses null checking and should be rare. Valid uses include immediately
after initialization where TypeScript's flow analysis doesn't understand that
initialization guarantees a value. Most non-null assertions indicate we should
restructure code to make the guarantee provable or handle the null case
properly.
Optional parameters in functions make intent explicit. When a parameter might
not be provided, marking it optional function greet(name?: string) documents
this in the type signature. The function body can then handle undefined
appropriately. This beats using | undefined explicitly and clarifies which
parameters callers must provide versus which are optional.
Modern TypeScript Type System
Utility types transform existing types without manually rewriting them.
Partial<T> makes all properties optional, useful for update operations where
only some fields change. Pick<T, K> extracts specific properties, creating
focused types from larger interfaces. Omit<T, K> excludes properties, useful
for derived types. Required<T> makes all properties required, inverting
Partial. These utilities express intent clearly and update automatically when
the base type changes.
Const assertions preserve literal types instead of widening to general types.
The expression { x: 10, y: 20 } as const creates type
{ readonly x: 10; readonly y: 20 } rather than { x: number; y: number }.
This enables precise typing of configuration objects, discriminated unions, and
other cases where specific values matter. Arrays with as const become readonly
tuples with exact types.
Template literal types create unions of strings matching patterns. The type
type Color = `#${string}` matches any string starting with #. Combined
with literal unions, this creates sophisticated string patterns:
type CSSVar = `--${string}`. These types make string manipulation
type-safe in ways plain string types can't.
Conditional types enable type-level computation: T extends U ? X : Y chooses
types based on conditions. This powers library types like NonNullable<T> which
removes null and undefined. When creating reusable type utilities, conditional
types provide necessary flexibility. They can feel complex but make formerly
impossible generic code type-safe.
Mapped types transform object types: { [K in keyof T]: T[K][] } converts all
properties to arrays of their original type. This pattern creates variations of
existing types programmatically. Combined with conditional types and utility
types, mapped types enable sophisticated type transformations that update
automatically when source types change.
Indexed access types extract property types from objects: User['email'] gets
the email property's type. This creates types derived from existing types,
maintaining consistency when the source type changes. It's particularly useful
for accessing nested types without duplicating their definitions.
The satisfies operator validates that a value matches a type without widening
its type. The expression config satisfies Config verifies config matches
Config but preserves the specific literal types in config. This solves the
common problem where type annotations widen types and lose precision.
React and Frontend TypeScript
Component props deserve careful typing. We define props as interfaces with clear
names: interface ButtonProps. This makes the component API explicit and
enables IntelliSense. Props should be as specific as practical—instead of
onClick: Function, use onClick: () => void or more specifically
onClick: React.MouseEventHandler<HTMLButtonElement>. Precise types catch
mistakes and document intended usage.
Event handlers in React have specific types that vary by element.
ChangeEvent<HTMLInputElement> types change events on inputs,
MouseEvent<HTMLButtonElement> types mouse events on buttons. Using correct
event types enables type-safe access to event properties without assertions.
Generic Event types lose specificity and require type assertions to access
element-specific properties.
Hooks require careful typing especially for custom hooks. The return type
[state, setState] should use as const so TypeScript infers a tuple rather
than an array. Dependencies arrays in useEffect and similar hooks should be
fully typed—TypeScript can verify all dependencies are listed when types are
specific. Missing dependencies cause bugs that proper typing helps prevent.
Generic components let us create reusable components with type-safe data. A
List<T> component can render any type of items while maintaining type safety
for item properties. The pattern
function List<T>({ items, renderItem }: ListProps<T>) makes the component
flexible while preventing type mismatches between items and rendering logic.
Context values need full typing to provide type safety throughout the component
tree. An untyped context value (or one typed as any) defeats TypeScript's
purpose. We define the context value type explicitly and use it consistently:
const UserContext = createContext<UserContextValue | undefined>(undefined).
This makes context values type-safe everywhere they're consumed.
Bringing It Together
Effective TypeScript code review focuses on type safety as the primary goal.
TypeScript's value comes from catching bugs at compile time that JavaScript
would only reveal at runtime. Code that bypasses type checking with any or
excessive type assertions loses this value. When reviewing TypeScript, we verify
the code leverages types to prevent bugs, not just satisfy the compiler.
Not all type issues are equal. A missing return type on a private function might
not matter, while any in a public API undermines type safety for all callers.
Type assertions in test code might be pragmatic, while type assertions in
production code often hide real problems. The key is understanding when type
safety matters most and ensuring critical code paths are fully type-safe.
TypeScript's type system continues evolving with each release. Recent versions
add features like satisfies, improved type narrowing, and better inference.
Teams benefit from staying current with TypeScript versions and adopting new
features that improve type safety. Code review creates opportunities to share
knowledge about these features and discuss when to use them effectively.

