Le Do Nghiem

Le Do Nghiem

AI Engineer

About meBooksSnippetsBlog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Blog

Mastering TypeScript: Tips and Tricks

Le Do Nghiem
Le Do NghiemAI Engineer
2025-09-10 4 min read
Share

A quick note first

TypeScript pays off when it stops bad states at compile time, not when it makes types harder to read than the runtime code.

These are patterns I use weekly — each one fixed a real bug or saved a refactor. I skip the exotic stuff unless the domain demands it.


1. Union types for allowed values

What: Narrow strings to a fixed set.

type UserRole = "admin" | "editor" | "viewer";

interface User {
  id: number;
  name: string;
  role: UserRole;
}

function canEdit(user: User) {
  return user.role === "admin" || user.role === "editor";
}

Why: Typos in role strings become compile errors, not silent wrong branches.

When not to: Fifty roles maintained in DB — use string at the boundary, validate with zod at runtime.


2. Discriminated unions

What: A shared kind or type field lets TypeScript narrow safely.

type Result =
  | { ok: true; data: string }
  | { ok: false; error: string };

function handle(result: Result) {
  if (result.ok) {
    console.log(result.data);
  } else {
    console.error(result.error);
  }
}

Why: No optional data? and error? on the same object — impossible states stay unrepresentable.

Red flag: Giant unions without a discriminator — if ('data' in x) works but gets messy.


3. satisfies — check shape, keep inference

What: Validate an object matches a type without widening literals.

type Route = { path: string; label: string };

const routes = [
  { path: "/", label: "Home" },
  { path: "/blog", label: "Blog" },
] satisfies Route[];

// routes[0].path is still "/" not string — autocomplete kept

Why: Config objects and theme tokens stay typed and literal.


4. as const for tuples and maps

const STATUS = ["pending", "done", "failed"] as const;
type Status = (typeof STATUS)[number];

Why: One source of truth for runtime array and compile-time union.


5. Utility types — Partial, Pick, Omit

interface User {
  id: number;
  name: string;
  email: string;
}

type UserUpdate = Partial<Pick<User, "name" | "email">>;

Why: PATCH endpoints and form state do not need a hand-rolled duplicate interface.

When not to: Over-Pick/Omit until the type is unreadable — sometimes a small dedicated type is clearer.


6. Strict null checks — embrace undefined

What: strict: true in tsconfig — especially strictNullChecks.

function getName(user: User | undefined) {
  if (!user) return "Guest";
  return user.name;
}

Why: Most production bugs I have seen are null/undefined assumptions.

Red flag: ! non-null assertion to silence the compiler. Fix the flow instead.


Bonus: narrow with in and type guards

When unions get wider, I use a kind field or in checks instead of casting.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function area(shape: Shape) {
  if (shape.kind === "circle") return Math.PI * shape.radius ** 2;
  return shape.width * shape.height;
}

Same idea as discriminated unions — keeps math functions honest at compile time.


Habits that stuck vs habits I dropped

  • Types as documentation for fantasy APIs — types drift from runtime; validate at boundaries (zod, valibot).
  • Generic soup — function foo<T extends U extends V> when a simple interface worked.
  • Ignoring unknown from JSON — as MyType without parsing is hope, not typing.

Where I'd start

Turn on strict if you have not. Pick one pattern from this list and apply it to the messiest module in your project — usually API responses or form state.

TypeScript is a linter for your assumptions. The best tricks are the ones that delete branches, not add them.

On this page

  • A quick note first
  • 1. Union types for allowed values
  • 2. Discriminated unions
  • 3. satisfies — check shape, keep inference
  • 4. as const for tuples and maps
  • 5. Utility types — Partial, Pick, Omit
  • 6. Strict null checks — embrace undefined
  • Bonus: narrow with in and type guards
  • Habits that stuck vs habits I dropped
  • Where I'd start
Share
Previous Post

React Performance Optimization Guide

Next Post

How I Build Scalable Web Apps