Kodokon kodokon.com

Narrowing: typeof, in, instanceof, and type guards

You master type narrowing mechanisms and write your own predicates to make unions more reliable.

10 min · 3 questions

Open this lesson in Kodokon

Narrowing is the mechanism by which TypeScript refines a broad type (a union, unknown) into a more precise one by analyzing control flow. You already use it without thinking about it: every if on a typed value triggers an analysis. The compiler tracks three families of built-in guards: typeof for primitives, instanceof for class instances, and in for the presence of a property. Choosing them well spares you reckless casts with as, which switch verification off instead of guiding it.

TYPESCRIPT
function formatValue(
  value: string | number | Date
): string {
  if (typeof value === "string") {
    return value.trim();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return value.toFixed(2);
}
Each branch narrows the type; the last one can only be number.

The in operator is invaluable when you handle structurally different objects that have no discriminant field. Be careful though: in tests for the presence of a key, including inherited ones, not its type. On external data (APIs, JSON), it narrows according to your declarations, not according to what actually exists at runtime. The classic real-world trap: a typeof value === "object" check lets null through, because typeof null is "object" in JavaScript. Always add an explicit null check.

TYPESCRIPT
interface Cat { meow(): void }
interface Dog { bark(): void }

function speak(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}
Narrowing by property presence with the in operator.

When the guard logic becomes reusable or too complex for flow analysis, write a custom type guard: a function whose return type is annotated value is T. The compiler then trusts your implementation. It is a contract: if your check is incomplete, you reintroduce runtime errors that the type system will never see. Since TypeScript 5.5, simple predicates are inferred automatically in callbacks such as filter, but the explicit annotation remains the norm for exported functions.

TYPESCRIPT
interface User { id: string; email: string }

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string" &&
    "email" in value &&
    typeof value.email === "string"
  );
}

const raw: unknown =
  JSON.parse('{"id":"1","email":"a@b.c"}');
if (isUser(raw)) {
  console.log(raw.email.toLowerCase());
}
A predicate that validates external data before use.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why is the check typeof value === "object" not enough to narrow to a non-null object?
    • Because typeof returns "Object" with a capital letter
    • Because typeof null is also "object" in JavaScript
    • Because typeof does not work on unions
    • Because arrays return "array"
  2. Which return annotation turns a function into a custom type guard?
    • boolean
    • value as User
    • value is User
    • asserts value
  3. What is the main risk of a badly written custom type guard?
    • A compile-time error inside the function
    • A lying narrowing: the compiler believes a type that is wrong at runtime
    • A loss of compilation performance