Type guards are expressions that perform runtime checks to narrow types.

typeof Type Guards

Use typeof to check primitive types.

function process(value: string | number) {
  if (typeof value === "string") {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  } else {
    // TypeScript knows value is number here
    console.log(value.toFixed(2));
  }
}

instanceof Type Guards

Use instanceof to check class instances.

class Dog {
  bark(): void {
    console.log("Woof!");
  }
}

class Cat {
  meow(): void {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark(); // TypeScript knows it's a Dog
  } else {
    animal.meow(); // TypeScript knows it's a Cat
  }
}

in Operator Type Guards

Use in to check for properties.

interface Dog {
  type: "dog";
  bark(): void;
}

interface Cat {
  type: "cat";
  meow(): void;
}

function makeSound(pet: Dog | Cat) {
  if ("bark" in pet) {
    pet.bark(); // TypeScript knows it's a Dog
  } else {
    pet.meow(); // TypeScript knows it's a Cat
  }
}

User-Defined Type Guards

Create custom type guard functions.

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // TypeScript knows it's a Fish
  } else {
    pet.fly(); // TypeScript knows it's a Bird
  }
}

Type Guard with Discriminated Union

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

function isCircle(shape: Shape): shape is { kind: "circle"; radius: number } {
  return shape.kind === "circle";
}

function area(shape: Shape): number {
  if (isCircle(shape)) {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.width * shape.height;
  }
}

Assertion Functions

Functions that assert a condition (TypeScript 3.7+).

function assertIsNumber(value: unknown): asserts value is number {
  if (typeof value !== "number") {
    throw new Error("Expected number");
  }
}

function process(value: unknown) {
  assertIsNumber(value);
  // TypeScript knows value is number here
  console.log(value.toFixed(2));
}

Exercises

Exercise 1: typeof Guards

// Create a function that:
// - Takes string | number | boolean
// - Uses typeof to handle each case
// - Returns a formatted string for each type

Exercise 2: instanceof Guards

// Create classes Vehicle, Car, and Bike
// Create a function that uses instanceof to:
// - Call drive() for Vehicle
// - Call specific methods for Car and Bike

Exercise 3: User-Defined Guards

// Create a type guard 'isString' that:
// - Takes unknown
// - Returns value is string
// - Checks if value is actually a string

Exercise 4: Discriminated Union Guards

// Create a discriminated union for API responses
// Create type guards for success and error cases
// Use them to safely handle responses

Exercise 5: Assertion Functions

// Create an assertion function 'assertIsArray' that:
// - Takes unknown
// - Asserts it's an array
// - Throws if not

Best Practices

  1. Use type guards instead of assertions - More type-safe
  2. Create reusable type guards - For common checks
  3. Use discriminated unions - Makes narrowing easier
  4. Document type guards - Explain what they check
  5. Test type guards - Ensure they work correctly

Common Mistakes

  1. Not using type guards
// ❌ Using type assertion
function process(value: string | number) {
  const str = value as string; // Unsafe!
  str.toUpperCase();
}

// ✅ Using type guard
function process(value: string | number) {
  if (typeof value === "string") {
    value.toUpperCase(); // Safe
  }
}
  1. Wrong type guard syntax
// ❌ Wrong return type
function isString(value: unknown): boolean {
  return typeof value === "string";
}

// ✅ Correct - type predicate
function isString(value: unknown): value is string {
  return typeof value === "string";
}
  1. Not handling all cases
// ❌ Missing else case
function process(value: string | number) {
  if (typeof value === "string") {
    value.toUpperCase();
  }
  // value might still be number here
}

// ✅ Handle all cases
function process(value: string | number) {
  if (typeof value === "string") {
    value.toUpperCase();
  } else {
    value.toFixed(2);
  }
}

Next Steps

Learn about Type Narrowing.