Type guards are a way to ensure that a variable or parameter is of a certain type within a specific block of code. They help TypeScript understand the type of a variable at a given point in the code, allowing for more precise type checking and reducing the likelihood of runtime errors.

Key Concepts

  1. Type Guards Basics:

    • Type guards are expressions that perform runtime checks to ensure a variable is of a specific type.
    • They help TypeScript narrow down the type of a variable within a conditional block.
  2. Using typeof:

    • The typeof operator can be used to check primitive types like string, number, boolean, etc.
  3. Using instanceof:

    • The instanceof operator checks if an object is an instance of a specific class.
  4. User-Defined Type Guards:

    • Custom functions that return a type predicate to perform more complex type checks.

Practical Examples

Using typeof

function printValue(value: string | number) {
    if (typeof value === "string") {
        console.log(`String value: ${value}`);
    } else {
        console.log(`Number value: ${value}`);
    }
}

printValue("Hello, TypeScript!"); // Output: String value: Hello, TypeScript!
printValue(42); // Output: Number value: 42

Explanation:

  • The typeof operator is used to check if value is a string or number.
  • TypeScript narrows down the type within each block, allowing for type-specific operations.

Using instanceof

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

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

function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark();
    } else {
        animal.meow();
    }
}

const myDog = new Dog();
const myCat = new Cat();

makeSound(myDog); // Output: Woof!
makeSound(myCat); // Output: Meow!

Explanation:

  • The instanceof operator checks if animal is an instance of Dog or Cat.
  • TypeScript narrows down the type within each block, allowing for class-specific method calls.

User-Defined Type Guards

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();
    } else {
        pet.fly();
    }
}

const myFish: Fish = { swim: () => console.log("Swimming...") };
const myBird: Bird = { fly: () => console.log("Flying...") };

move(myFish); // Output: Swimming...
move(myBird); // Output: Flying...

Explanation:

  • The isFish function is a user-defined type guard that checks if pet is a Fish.
  • The return type pet is Fish is a type predicate that tells TypeScript the type of pet within the if block.

Practical Exercises

Exercise 1: Using typeof

Write a function describeValue that takes a parameter of type string | number | boolean and logs a different message for each type.

function describeValue(value: string | number | boolean) {
    // Your code here
}

// Test cases
describeValue("Hello");
describeValue(100);
describeValue(true);

Solution:

function describeValue(value: string | number | boolean) {
    if (typeof value === "string") {
        console.log(`This is a string: ${value}`);
    } else if (typeof value === "number") {
        console.log(`This is a number: ${value}`);
    } else {
        console.log(`This is a boolean: ${value}`);
    }
}

// Test cases
describeValue("Hello"); // Output: This is a string: Hello
describeValue(100); // Output: This is a number: 100
describeValue(true); // Output: This is a boolean: true

Exercise 2: Using instanceof

Create a function identifyShape that takes a parameter of type Circle | Square and logs a different message for each shape.

class Circle {
    radius: number;
    constructor(radius: number) {
        this.radius = radius;
    }
}

class Square {
    sideLength: number;
    constructor(sideLength: number) {
        this.sideLength = sideLength;
    }
}

function identifyShape(shape: Circle | Square) {
    // Your code here
}

// Test cases
const myCircle = new Circle(10);
const mySquare = new Square(5);

identifyShape(myCircle);
identifyShape(mySquare);

Solution:

class Circle {
    radius: number;
    constructor(radius: number) {
        this.radius = radius;
    }
}

class Square {
    sideLength: number;
    constructor(sideLength: number) {
        this.sideLength = sideLength;
    }
}

function identifyShape(shape: Circle | Square) {
    if (shape instanceof Circle) {
        console.log(`This is a circle with radius: ${shape.radius}`);
    } else {
        console.log(`This is a square with side length: ${shape.sideLength}`);
    }
}

// Test cases
const myCircle = new Circle(10);
const mySquare = new Square(5);

identifyShape(myCircle); // Output: This is a circle with radius: 10
identifyShape(mySquare); // Output: This is a square with side length: 5

Exercise 3: User-Defined Type Guards

Create a function isRectangle that checks if a shape is a Rectangle and use it in a function describeShape to log different messages for Rectangle and Triangle.

interface Rectangle {
    width: number;
    height: number;
}

interface Triangle {
    base: number;
    height: number;
}

function isRectangle(shape: Rectangle | Triangle): shape is Rectangle {
    // Your code here
}

function describeShape(shape: Rectangle | Triangle) {
    // Your code here
}

// Test cases
const myRectangle: Rectangle = { width: 10, height: 20 };
const myTriangle: Triangle = { base: 5, height: 10 };

describeShape(myRectangle);
describeShape(myTriangle);

Solution:

interface Rectangle {
    width: number;
    height: number;
}

interface Triangle {
    base: number;
    height: number;
}

function isRectangle(shape: Rectangle | Triangle): shape is Rectangle {
    return (shape as Rectangle).width !== undefined;
}

function describeShape(shape: Rectangle | Triangle) {
    if (isRectangle(shape)) {
        console.log(`This is a rectangle with width: ${shape.width} and height: ${shape.height}`);
    } else {
        console.log(`This is a triangle with base: ${shape.base} and height: ${shape.height}`);
    }
}

// Test cases
const myRectangle: Rectangle = { width: 10, height: 20 };
const myTriangle: Triangle = { base: 5, height: 10 };

describeShape(myRectangle); // Output: This is a rectangle with width: 10 and height: 20
describeShape(myTriangle); // Output: This is a triangle with base: 5 and height: 10

Common Mistakes and Tips

  • Mistake: Forgetting to use type guards within conditional blocks.

    • Tip: Always use type guards to narrow down types before performing type-specific operations.
  • Mistake: Using typeof for non-primitive types.

    • Tip: Use typeof for primitive types and instanceof for class instances.
  • Mistake: Not returning a type predicate in user-defined type guards.

    • Tip: Ensure your custom type guard functions return a type predicate (param is Type).

Conclusion

Type guards are a powerful feature in TypeScript that help ensure type safety and reduce runtime errors. By using built-in operators like typeof and instanceof, as well as creating user-defined type guards, you can write more robust and maintainable code. Practice using type guards in different scenarios to become proficient in leveraging this feature in your TypeScript projects.

© Copyright 2024. All rights reserved