Stop trying `typeof x === Fish`: A practical guide to TypeScript type verification (Narrowing + Type Predicates)

Published: (January 5, 2026 at 09:45 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Stop trying typeof x === Fish: A practical guide to TypeScript type verification (Narrowing + Type Predicates)

When you come from a “typed mindset”, it’s natural to want something like:

typeof animal === Fish

But JavaScript doesn’t work that way.

1) The key idea: types don’t exist at runtime

TypeScript types are erased after compilation. At runtime, you only have JavaScript values.
Runtime checks are always things like:

  • typeof x === "string"
  • x instanceof Date
  • "swim" in animal
  • animal.kind === "fish"

TypeScript then uses those checks to narrow union types.

2) Narrowing: TypeScript understands common JS patterns

typeof narrowing (primitives)

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") return " ".repeat(padding) + input;
  return padding + input;
}

instanceof narrowing (classes / constructors)

function logValue(x: Date | string) {
  if (x instanceof Date) return x.toUTCString();
  return x.toUpperCase();
}

"in" narrowing (property existence)

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) animal.swim();
  else animal.fly();
}

Note: in works on the prototype chain, and optional properties affect both branches.

3) Type predicates: make narrowing reusable (the real win)

When you need to reuse a check across multiple places (e.g., in .filter()), type predicates (user‑defined type guards) shine:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) animal.swim();
  else animal.fly();
}

Now the same function can be reused:

const zoo: (Fish | Bird)[] = [/* ... */];
const fishes = zoo.filter(isFish); // Fish[]

Another example with discriminated props:

type ButtonAsLink = { href: string; onClick?: never };
type ButtonAsAction = { onClick: () => void; href?: never };
type Props = { label: string } & (ButtonAsLink | ButtonAsAction);

function isLinkProps(p: Props): p is Props & ButtonAsLink {
  return "href" in p;
}

function SmartButton(props: Props) {
  if (isLinkProps(props)) {
    return {props.label};
  }
  return {props.label};
}

4) Best practice when you control the model: discriminated unions

If you can change the data shape, use a discriminated union for the most robust approach:

type Fish = { kind: "fish"; swim: () => void };
type Bird = { kind: "bird"; fly: () => void };

function move(animal: Fish | Bird) {
  if (animal.kind === "fish") animal.swim();
  else animal.fly();
}

This is clearer than property checks and scales well as unions grow.

5) Common pitfalls (learn these once)

  • typeof null === "object" (historic JS quirk)
  • !value checks falsy values (0, "", false) — not just null/undefined
  • "prop" in obj can be true because of prototypes
  • Optional properties can cause both branches to still include a type (e.g., Human may have swim?())

Takeaway

  • Runtime verification comes from JavaScript checks.
  • Compile‑time safety comes from TypeScript narrowing.
  • When you want reuse, wrap the check in a type predicate.

“Use JS checks to narrow, and type predicates to reuse the narrowing.”

Back to Blog

Related posts

Read more »

Anguar Tips #4

Introduction Some tips for working with Angular – from a frontend developer Part 4. These tips assume you already have experience with Angular, so we won’t div...