YouTip LogoYouTip

Ts Type Guards

TypeScript Type Guards |

Type Guards are an important type narrowing mechanism in TypeScript.

They allow developers to accurately infer variable types at runtime through specific conditional checks.

With type guards, we can safely access properties and methods of specific types within union type variables.


Why Type Guards Are Needed

In TypeScript, a variable may be declared as a union of multiple types.

When we need to perform different operations based on different types, the compiler cannot automatically determine the current specific type.

Type Guards are the key mechanism to solve this problem.

Concept Explanation: The core principle of Type Guards is "Type Narrowing". Through conditional judgments, TypeScript will automatically narrow down the union type to a specific type.



typeof Type Guard

typeof is the most commonly used type guard, used to check primitive types (string, number, boolean, etc.).

It returns a string representing the type of the value.

Basic Usage of typeof

// Define a function that accepts a union type

// Parameter value could be string or number

function printValue(value: string | number):void{

    // Use typeof to check type

    // When the condition in if is true, TypeScript will automatically narrow value to string type

    if(typeof value ==="string"){

        // At this point, TypeScript knows value is string

        // Can safely access the length property of string

        console.log("String length: "+ value.length);

    }else{

        // When entering else branch, TypeScript knows value is not string

        // It can only be number type

        // Can safely perform mathematical operations

        console.log("Double the number: "+(value *2));

    }

}

// Test call

printValue("hello");// Pass in string

printValue(42);// Pass in number

Output:

String length: 5
Double the number: 84

Types Supported by typeof:

  • "string" - String type
  • "number" - Number type (including NaN and Infinity)
  • "boolean" - Boolean type
  • "undefined" - Undefined type
  • "object" - Object type (Note: Arrays and null will also be identified as "object")
  • "function" - Function type

Note: typeof returns "object" for both arrays and null. If you need to distinguish between arrays and objects precisely, other methods are required.


instanceof Type Guard

instanceof is used to check whether an object is an instance of a certain class.

It determines the type by checking the object's prototype chain.

Basic Usage of instanceof

// Define Dog class

class Dog {

    // Method for dog's bark

    bark():void{

        console.log("Woof woof!");

    }

}

// Define Cat class

class Cat {

    // Method for cat's meow

    meow():void{

        console.log("Meow meow!");

    }

}

// Function accepting union type

function makeSound(animal: Dog | Cat):void{

    // Use instanceof to check if animal is Dog or Cat

    // When the condition in if is true, TypeScript narrows animal to Dog type

    if(animal instanceof Dog){

        // Now we can call Dog-specific methods

        animal.bark();

    }else{

        // In else branch, TypeScript narrows animal to Cat type

        animal.meow();

    }

}

// Test call

makeSound(new Dog());// Create Dog instance and call

makeSound(new Cat());// Create Cat instance and call

Output:

Woof woof!
Meow meow!

Explanation:

instanceof checks the object's prototype chain, so it can only be used with class instances, not interfaces or type aliases.


Custom Type Guards

When built-in typeof and instanceof do not meet requirements, custom type guard functions can be created.

Custom type guards use the return type syntax value is Type.

Custom Guard Functions

// Define a custom type guard function

// Return type uses "value is Type" format

// This tells TypeScript: when the function returns true, the parameter type is string

function isString(value: any): value is string {

    // Use typeof to check if it's a string

    return typeof value ==="string";

}

// Another custom guard: check if it's a number

function isNumber(value: any): value is number {

    return typeof value ==="number";

}

// Define an array type guard

function isArray(value: any): value is any[]{

    return Array.isArray(value);

}

// Function to process values

function processValue(value: string | number | any[]):void{

    // Use custom guards for type checking

    if(isString(value)){

        // TypeScript knows value is string type

        // Can call toUpperCase() method

        console.log("String to uppercase: "+ value.toUpperCase());

    }else if(isNumber(value)){

        // TypeScript knows value is number type

        // Can call toFixed() method

        console.log("Number formatted: "+ value.toFixed(2));

    }else if(isArray(value)){

        // TypeScript knows value is array type

        console.log("Array length: "+ value.length);

    }

}

// Test calls

processValue("hello");

processValue(3.14159);

processValue([1,2,3,4,5]);

Output:

String to uppercase: HELLO
Number formatted: 3.14
Array length: 5

Tip: The key to custom type guards is the return type value is Type, which is the identifier that TypeScript uses to recognize type guards.


in Operator Type Guard

The in operator can check if an object contains a specific property.

When used in conditional statements, TypeScript will automatically narrow the object's type range.

Usage of in Operator

// Define two interfaces with different properties

interface A {

    a: string;// Only property a

}

interface B {

    b: number;// Only property b

}

// Function accepting union type

function process(obj: A | B):void{

    // Use in to check if object has property "a"

    if("a"in obj){

        // In if branch, TypeScript knows obj contains property a

        // So obj's type is narrowed to A

        console.log("Property a of A: "+ obj.a);

    }else{

        // In else branch, obj does not contain property a

        // TypeScript knows obj must be type B

        // So we can safely access property b

        console.log("Property b of B: "+ obj.b);

    }

}

// Test calls

process({ a:"hello"});// Pass in object with property a

process({ b:42});// Pass in object with property b

Output:

Property a of A: hello
Property b of B: 42

Discriminated Unions and Type Guards

A discriminated union is a powerful pattern that distinguishes members of a union type through a common "identifier" property.

Combined with switch statements or if checks, it enables complete type guarding.

Discriminated Union Implementation - Calculator

// Define Circle interface, using kind property as identifier

interface Circle {

    kind:"circle";// Identifier field: value is "circle"

    radius: number;// Radius

}

// Define Rectangle interface

interface Rectangle {

    kind:"rectangle";// Identifier field: value is "rectangle"

    width: number;// Width

    height: number;// Height

}

// Define Triangle interface

interface Triangle {

    kind:"triangle";// Identifier field: value is "triangle"

    base: number;// Base

    height: number;// Height

}

// Define union type

type Shape = Circle | Rectangle | Triangle;

// Function to calculate area

function getArea(shape: Shape): number {

    // Use switch statement for type guarding

    // Based on the value of kind property, TypeScript will automatically narrow the type

    switch(shape.kind){

        case"circle":

            // shape is narrowed to Circle type

            // Can access radius property

            return Math.PI* shape.radius**2;

        case"rectangle":

            // shape is narrowed to Rectangle type

            // Can access width and height properties

            return shape.width* shape.height;

        case"triangle":

            // shape is narrowed to Triangle type

            return 0.5* shape.base* shape.height;

    }

}

// Test calls

var circle ={ kind:"circle" as const, radius:5};

var rectangle ={ kind:"rectangle" as const, width:4, height:6};

var triangle ={ kind:"triangle" as const, base:3, height:4};

console.log("Circle area: "+ getArea(circle).toFixed(2));

console.log("Rectangle area: "+ getArea(rectangle));

console.log("Triangle area: "+ getArea(triangle));

Output:

Circle area: 78.54
Rectangle area: 24
Triangle area: 6

Explanation: Discriminated unions are one of the most recommended patterns in TypeScript. They distinguish different type members through a common literal property (usually kind or type), making the code both type-safe and maintainable.


Null and Undefined Checks

When handling values that might be null or undefined, direct equality checks are also effective type guards.

Null Check

// Define a function parameter that might be null

function getLength(str: string |null): number {

    // Directly check if str is not null

    // When condition is true, TypeScript knows str is not null

    // At this point, we can safely access str's properties

    if(str !==null){

        return str.length;

    }

    // Handle null case

    return 0;

}

// Test calls

console.log(getLength("hello"));// Normal string

console.log(getLength(null));// Pass in null

Output:

5
0

Tip: After enabling strictNullChecks, it is recommended to always perform null checks. Optional chaining (?.) and nullish coalescing (??) can simplify code.


Truthiness Narrowing

Besides explicit type checks, TypeScript also narrows types through truthiness assertions.

Truthiness Narrowing

// String that might be undefined

function greet(name?: string): string {

    // Use short-circuit operator: if name is undefined or empty string, use default value

    // In the code block after &&, TypeScript knows name definitely has a value

    return name &&"Hello, "+ name;

}

// Test

console.log(greet("TUTORIAL"));

console.log(greet());

Output:

Hello, TUTORIAL
Hello, undefined

Important Notes

  • Type guards must be used in conditional branches: Only after using type guards in conditional judgments will TypeScript perform type narrowing
  • Return type must be a type predicate: The return type of custom type guards must be in the format value is Type
  • Discriminated unions are best practices: For complex union types, it is recommended to use the discriminated union pattern
  • Be mindful of completeness of type narrowing: When using switch statements, it is recommended to handle all possible branches

Suggestion: When dealing with union types, prioritize using the discriminated union pattern. It not only makes the code clearer but also fully leverages TypeScript's type inference capabilities.


Summary

Type guards are an essential part of TypeScript's type system.

  • typeof: Most commonly used, suitable for checking primitive types
  • instanceof: Checks if an object is an instance of a specific class
  • Custom guards: Achieve flexible class checks using the value is Type syntax
  • in: Checks if an object contains a specific property
  • Discriminated unions: Recommended best practice pattern, distinguishing types via identifier fields
  • Truthiness narrowing: Uses JavaScript's truthiness judgment to narrow types
← Ts Utility TypesTs Mixin β†’