Skip to content

TypeScript Type Guards — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Type Guards. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript type guards are runtime checks that inform the compiler about a value's specific type within a code branch — they transform broad types like string | number into narrow, usable types through typeof, instanceof, in, and custom predicate functions.

What You'll Learn

  • typeof type guards for primitives
  • instanceof type guards for classes
  • in type guards for property existence
  • User-defined type predicates (is)
  • Discriminated unions with kind properties

Why It Matters

Without type guards, every value of a union type is limited to the intersection of all members' properties. Type guards unlock the full interface of each member, enabling type-safe access to properties that exist only on specific variants.

Real-World Use

Durga Antivirus Pro's threat detection uses a discriminated union for scan results — a scan can be { status: "clean" }, { status: "infected"; threats: string[] }, or { status: "error"; message: string }. Type guards on the status property ensure only the relevant fields are accessed for each variant.

Learning Path

flowchart LR
  A[Index Signatures] --> B[Type Guards]
  B --> C[Narrowing]
  B --> D[You Are Here]
  C --> E[Recursive Types]
  E --> F[Variance]

typeof Type Guard

For primitive types, typeof is the simplest type guard:

function process(value: string | number | boolean): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // value is string here
  } else if (typeof value === "number") {
    return value.toFixed(2); // value is number here
  } else {
    return value ? "true" : "false"; // value is boolean here
  }
}

console.log(process("hello")); // HELLO
console.log(process(42.123));  // 42.12
console.log(process(true));    // true

Think of typeof as asking: "What primitive type is this value?" TypeScript narrows the type based on each possible result.

What typeof Can Check

typeof "hello"   // "string"
typeof 42        // "number"
typeof true      // "boolean"
typeof undefined // "undefined"
typeof Symbol()  // "symbol"
typeof 123n      // "bigint"
typeof function(){} // "function"
typeof {}        // "object"
typeof null      // "object" (bug — use value === null instead)

instanceof Type Guard

For class instances, instanceof checks the Prototype chain:

class APIError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}

class NetworkError extends Error {
  constructor(public url: string, message: string) {
    super(message);
  }
}

function handleError(error: APIError | NetworkError): string {
  if (error instanceof APIError) {
    return `API Error ${error.statusCode}: ${error.message}`;
  } else {
    return `Network Error on ${error.url}: ${error.message}`;
  }
}

console.log(handleError(new APIError(404, "Not found")));
// API Error 404: Not found

console.log(handleError(new NetworkError("https://api.example.com", "Timeout")));
// Network Error on https://api.example.com: Timeout

in Type Guard

Check for property existence to narrow object types:

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

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

User-Defined Type Predicates

Custom functions that return a type predicate (parameterName is Type):

interface Cat {
  meow(): void;
  purr(): void;
}

interface Dog {
  bark(): void;
  wagTail(): void;
}

function isCat(pet: Cat | Dog): pet is Cat {
  return "meow" in pet;
}

function handlePet(pet: Cat | Dog): void {
  if (isCat(pet)) {
    pet.meow(); // Narrowed to Cat
    pet.purr();
  } else {
    pet.bark(); // Narrowed to Dog
    pet.wagTail();
  }
}

Complex Predicate Example

interface User {
  type: "user";
  name: string;
  email: string;
}

interface Admin {
  type: "admin";
  name: string;
  permissions: string[];
}

function isAdmin(user: User | Admin): user is Admin {
  return user.type === "admin";
}

function getPermissions(user: User | Admin): string[] {
  if (isAdmin(user)) {
    return user.permissions; // Narrowed to Admin
  }
  return ["read"]; // Default for regular users
}

Discriminated Unions

A discriminated union has a common property (the discriminant) with literal types:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

The never Exhaustiveness Check

Use never to ensure all union members are handled:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape); // Compile error if a case is missing
  }
}

Null/Undefined Guards

function findUser(id: string): User | null | undefined {
  // ...
}

function displayName(id: string): string {
  const user = findUser(id);

  if (user === null) {
    return "User not found";
  }

  if (user === undefined) {
    return "Not initialized";
  }

  return user.name; // Narrowed to User
}

Truthiness/Falsiness Guard

function getLength(value: string | null | undefined): number {
  // Falsy check catches null, undefined, and empty string
  if (!value) return 0;
  return value.length; // Narrowed to string
}

Common Mistakes

1. Using typeof for Objects

if (typeof myVar === "object") {
  // myVar could be: object, array, null, Date, etc.
  // Not narrowed enough for practical use
}

Use in, instanceof, or custom predicates instead.

2. Forgetting That null Is "object" with typeof

if (typeof value === "object" && value !== null) {
  // Safe: value is a non-null object
}

3. Not Providing Exhaustiveness Check

Without assertNever, adding a new union member won't cause compile errors — you'll get runtime bugs.

4. Overusing User-Defined Type Predicates

If a simple in or typeof check works, prefer it over a custom function. Predicates are best for complex logic.

5. Type Predicate Without Runtime Logic

function isString(x: unknown): x is string {
  return true; // Always returns true — incorrect!
}

The predicate must match the actual runtime behavior.

Practice Questions

  1. What is the difference between typeof and instanceof? typeof checks primitive types (string, number, etc.). instanceof checks class/constructor prototype chains.

  2. What is a user-defined type predicate? A function returning x is Type that tells TypeScript to narrow the type when the function returns true.

  3. What is a discriminated union? A union type where each member has a common literal property (the discriminant) that TypeScript uses for narrowing.

  4. How does the never exhaustiveness check work? After handling all known union members, the remaining type should be never. If a new member is added, the never check fails with a compile error.

Challenge: Create a discriminated union ApiResponse<T> with { status: "loading" }, { status: "success"; data: T }, and { status: "error"; message: string }. Write a function that handles each case and returns a React-ready string.

FAQ

Can I use `switch` for type narrowing?

Yes, especially with discriminated unions. TypeScript narrows within each case branch.

What is the difference between `in` and `hasOwnProperty`?

in checks the prototype chain. hasOwnProperty checks only own properties. Both work for type narrowing.

Can I use `Array.isArray()` as a type guard?

Yes. TypeScript natively recognizes Array.isArray(x) and narrows to unknown[].

What happens if I use a type predicate incorrectly?

TypeScript trusts your predicate. If the runtime check doesn't match the return type annotation, you'll get runtime bugs.

Do type guards affect performance?

Minimally. They are simple runtime checks (typeof, instanceof, property lookup). The benefit of catching type bugs far outweighs the cost.

Mini Project: Event Handler with Discriminated Unions

// src/event-handler.ts

type AppEvent =
  | { type: "user_login"; userId: string; timestamp: Date }
  | { type: "file_scan"; filePath: string; threats: number; duration: number }
  | { type: "threat_detected"; threatName: string; severity: "low" | "medium" | "high" | "critical" }
  | { type: "error"; code: number; message: string };

function handleEvent(event: AppEvent): void {
  switch (event.type) {
    case "user_login":
      console.log(`User ${event.userId} logged in at ${event.timestamp}`);
      break;

    case "file_scan":
      console.log(`Scanned ${event.filePath}: ${event.threats} threats in ${event.duration}ms`);
      break;

    case "threat_detected":
      const icon = event.severity === "critical" ? "🚨" : "⚠️";
      console.log(`${icon} ${event.severity.toUpperCase()}: ${event.threatName}`);
      break;

    case "error":
      console.error(`Error ${event.code}: ${event.message}`);
      break;

    default:
      const _exhaustive: never = event;
      throw new Error(`Unknown event type: ${_exhaustive}`);
  }
}

handleEvent({
  type: "user_login",
  userId: "usr-001",
  timestamp: new Date(),
});
// User usr-001 logged in at ...

handleEvent({
  type: "threat_detected",
  threatName: "Trojan.Generic",
  severity: "critical",
});
// CRITICAL: Trojan.Generic

What's Next

Now explore control flow analysis and narrowing in depth:

Lesson Description
{{< ref "/programming-languages/typescript/24-index-signatures" >}} Review index signatures
{{< ref "/programming-languages/typescript/26-narrowing" >}} Control flow analysis and exhaustiveness
{{< ref "/programming-languages/typescript/27-recursive-types" >}} Recursive and self-referential types

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro