TS

Type Guards & Narrowing

TypeScript · 7 entries

typeof Guard

syntax
if (typeof value === "string") { ... }
example
function formatValue(input: string | number | boolean): string {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  if (typeof input === "number") {
    return input.toFixed(2);
  }
  return input ? "Yes" : "No";
}
output
// TypeScript narrows the type inside each branch automatically

Note typeof works for: "string", "number", "boolean", "undefined", "symbol", "bigint", "function", "object". Note: typeof null === "object" — this is a famous JavaScript quirk that TypeScript inherits.

instanceof Guard

syntax
if (value instanceof ClassName) { ... }
example
class ApiError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}

class ValidationError extends Error {
  constructor(public fields: string[], message: string) {
    super(message);
  }
}

function handleError(err: Error) {
  if (err instanceof ApiError) {
    console.log(`API error ${err.statusCode}: ${err.message}`);
  } else if (err instanceof ValidationError) {
    console.log(`Invalid fields: ${err.fields.join(", ")}`);
  } else {
    console.log(`Unknown error: ${err.message}`);
  }
}
output
// instanceof narrows to the specific class, enabling access to class-specific properties

Note instanceof checks the prototype chain at runtime. It does not work with interfaces or type aliases (they have no runtime presence). It also fails across different realms (iframes) since each realm has its own constructor.

in Operator Guard

syntax
if ("property" in value) { ... }
example
interface EmailContact {
  email: string;
  name: string;
}

interface PhoneContact {
  phone: string;
  name: string;
}

function sendMessage(contact: EmailContact | PhoneContact) {
  if ("email" in contact) {
    console.log(`Emailing ${contact.email}`);
  } else {
    console.log(`Calling ${contact.phone}`);
  }
}
output
// 'in' narrows based on the presence of a property

Note The 'in' operator checks if a property name exists on an object at runtime. TypeScript uses this to narrow union types. The property name must be a string literal for narrowing to work. Works well when union members have distinct unique properties.

Custom Type Guard Functions

syntax
function isType(value: ParamType): value is TargetType {
  return /* boolean check */;
}
example
interface Fish {
  swim: () => void;
  habitat: "water";
}

interface Bird {
  fly: () => void;
  habitat: "air";
}

function isFish(creature: Fish | Bird): creature is Fish {
  return creature.habitat === "water";
}

function move(creature: Fish | Bird) {
  if (isFish(creature)) {
    creature.swim(); // TypeScript knows this is Fish
  } else {
    creature.fly();  // TypeScript knows this is Bird
  }
}
output
// Custom predicates with 'is' return type enable reusable narrowing

Note The 'value is Type' return annotation is a type predicate — it tells TypeScript to narrow the variable when the function returns true. The compiler trusts your implementation; if your check is wrong, you get silent type errors at runtime. Always keep the check accurate.

Discriminated Unions

syntax
interface A { kind: "a"; ... }
interface B { kind: "b"; ... }
type Union = A | B;
example
interface LoadingState {
  status: "loading";
}

interface SuccessState {
  status: "success";
  data: string[];
}

interface ErrorState {
  status: "error";
  errorMessage: string;
}

type RequestState = LoadingState | SuccessState | ErrorState;

function renderState(state: RequestState): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return `Got ${state.data.length} items`; // data is available
    case "error":
      return `Error: ${state.errorMessage}`; // errorMessage is available
  }
}
output
// Each case automatically narrows to the correct interface

Note A discriminated union has a common literal-typed property (the 'discriminant') shared across all members. TypeScript narrows exhaustively in switch/if on that property. This is the recommended pattern for modeling states, events, and message types.

Assertion Functions

syntax
function assert(value: unknown): asserts value is Type {
  if (!check) throw new Error(...);
}
example
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value == null) {
    throw new Error("Value must not be null or undefined");
  }
}

function processInput(data: unknown) {
  assertIsString(data);
  // After this line, data is typed as string
  console.log(data.toUpperCase());
}
output
// After the assertion call, TypeScript narrows for the rest of the scope

Note Assertion functions narrow by throwing on failure rather than returning a boolean. They use 'asserts value is Type' (or just 'asserts value' for truthiness). Unlike type predicates, they affect the current scope rather than an if branch. Must throw — not return false.

Truthiness Narrowing

syntax
if (value) { ... } // narrows out null, undefined, 0, "", false
example
function printLength(input: string | null | undefined) {
  if (input) {
    // input is narrowed to string (null and undefined excluded)
    console.log(`Length: ${input.length}`);
  } else {
    console.log("No input provided");
  }
}

// Combining with logical operators
function getDisplayName(first?: string, last?: string): string {
  return (first && last) ? `${first} ${last}` : first ?? last ?? "Anonymous";
}
output
// Truthiness checks exclude null, undefined, and falsy values

Note Truthiness narrowing also excludes 0, empty string, and false — which may not be what you want. If 0 or "" are valid values, check explicitly for null/undefined instead: if (value != null) or if (value !== undefined).