RizTech Academy logo
RizTech Academy
Writing TypeScript Worth ReadingLesson 5 of 525 min

Reading TypeScript like a reviewer

On a real team, almost nothing reaches production without another developer reading it first. This final best-practices lesson turns everything the module taught into a lens you can read code through — as the person being reviewed, and soon as a reviewer. It closes the module the way the whole course closes: write code for the human who reads it next.

Why review, and the mindset

Code review is not about typos — the compiler and linter catch those. It exists to catch bugs a test would miss, spread knowledge (now two people understand the code), keep the codebase coherent, and teach in both directions. The mindset that matters most: review the code, not the coder. "This can be null here" is useful; "you always forget null" is not. As the author, receive feedback the same way — it is about making the code better, not a verdict on you. Teams where review is a shared craft, not a gauntlet, ship better software and are better to work in.

The TypeScript reviewer's checklist

When you read a change — your own before submitting, or a teammate's — work down these, roughly in order of importance. Every item is a lesson from this course.

1. Type honesty — is the code lying to the compiler?

  • Any any that should be unknown or a real type? (prefer-unknown lesson.)
  • Any as or ! that should be a narrowing check or validation? (avoid-assertions lesson.)
  • Is external data validated (unknown + a guard/Zod), or just asserted to have a shape? (The most common real bug.)

2. Modelling — do the types tell the truth about the data?

  • Do the types allow illegal states? A boolean where a union fits, a string where a literal union fits, optionals that should be a discriminated union? (model-with-types lesson.)
  • Are failures modelled honestly — a Result for expected failures, throw for exceptional ones? (errors-typed lesson.)
  • Is a discriminated union handled exhaustively (the never check), so a new case cannot be silently dropped? (unions module.)

3. Correctness — does it actually work?

  • Nulls and undefined handled (not !'d away)?
  • Promises awaited (no "used a promise as a value")? Independent async work concurrent (Promise.all), not sequential?
  • Off-by-ones, empty collections, the right data structure (a Set for membership, not an array scan)?

4. Readability — can you understand it without asking?

  • Do names reveal intent? Are functions small and single-purpose?
  • Is it idiomatic TypeScript, or over-clever — a baroque conditional type where a simple one (or a runtime check) would do? (building-utility-types lesson: mastery is restraint.)

5. Tests — is it proven?

  • Is there a test for the new behaviour, including edge and failure cases? Would it actually fail if the code were wrong?

Reading a change: a worked eye

Here is a snippet as it might arrive in review. Read it as a reviewer before the notes:

async function loadUser(id: any) {
  const res = await fetch("/api/users/" + id);
  const user = await res.json() as User;
  return user.name!.toUpperCase();
}

A careful reviewer flags several things, each a lesson from this module:

  • id: any — should be string (or number); any here is a lie that lets any argument through. (prefer-unknown.)
  • res.json() as User — asserting the shape without checking it; if the API sends something else, this crashes. Should be unknown + a guard (or Zod). (avoid-assertions, prefer-unknown.)
  • user.name! — a non-null assertion that will crash if name is missing; the data was never validated, so this is a real risk. (avoid-assertions.)
  • No error handling — fetch failing or a non-OK response are unhandled; and the return type is unstated. (errors-typed.)

The improved version:

async function loadUser(id: string): Promise<Result<string>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
  const data: unknown = await res.json();
  if (!isUser(data)) return { ok: false, error: "Invalid user data" };
  return { ok: true, value: data.name.toUpperCase() };
}

Typed parameter, validated data, a Result return that makes failure visible and handled, and not a single lie to the compiler. A good review turns the first version into the second — not by rewriting it for the author, but by pointing at each issue so the author learns to see it.

Giving and receiving feedback well

The human half matters as much as the technical:

  • Be specific and kind. "Consider unknown + a guard here — as User would crash on bad data" is actionable. "This is unsafe" is not.
  • Distinguish must-fix from nice-to-have. A missing validation is a blocker; a naming preference is a suggestion. Say which is which.
  • Ask, do not command, on judgement calls. "Would a discriminated union be cleaner here?" invites thought; sometimes the author has a reason.
  • As the author, do not defend — understand. If a reviewer misread the code, that is a signal the code was unclear; explain, then usually make it clearer anyway.
  • Praise good types too. Noting an elegant discriminated union or a well-placed Result teaches and encourages.

The whole course, as one habit

Step back and see what this module — and this course — was really for. Null safety, unions, generics, the advanced type system, best practices: they are not separate topics. They are one habit — model reality honestly in types, do not lie to the compiler, and write for the human who reads next. The compiler does not care about your names or your modelling; the program runs the same. All of it is for the reader — your teammate, your reviewer, your future self — and for making bugs impossible rather than merely caught. A developer who internalises that — who reaches for unknown over any, models illegal states out, and keeps types as simple as the problem allows — is the one worth hiring and the one a team wants to keep. That is the difference this course has been building toward, and the capstone is where you put it into practice.

Check your work

What code review is for. Catching bugs tests miss, spreading knowledge, keeping the codebase coherent, and teaching — not typos.

The core mindset. Review the code, not the coder; feedback is about the change, never the person.

The five review dimensions. Type honesty, modelling, correctness, readability, tests.

Type-honesty things to check. any that should be unknown, as/! that should be narrowing, external data asserted instead of validated.

Modelling things to check. Types allowing illegal states, failures modelled with the right tool (Result vs throw), exhaustive union handling.

What makes a test worth having. It would actually fail if the code were wrong.

How to give feedback well. Specific and kind, must-fix versus nice-to-have, ask on judgement calls, praise good work.

The one habit behind the whole course. Model reality honestly in types, do not lie to the compiler, and write for the human who reads next.

Practice

  1. Review the buggy loadUser yourself before reading the notes. List every issue, then compare.
  2. Rewrite it into the safe version and confirm it validates data and returns a Result.
  3. Review a piece of your own TypeScript against the five-dimension checklist. Find at least three improvements.
  4. Rewrite a "this is unsafe" comment into specific, kind, actionable feedback.
  5. Find an any, an as, or a ! in code you can access and write the review note that would flag it.
  6. Find a type that allows illegal states in code you know and write the note suggesting a discriminated union.
  7. Write down, in your own words, the one-sentence habit this course comes down to, and pin it where you code.

Official documentation

Next module — Design Patterns in TypeScript: which patterns the type system dissolves, and which earn their place.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship