Open In App

Recursive Type Guards In TypeScript

Last Updated : 25 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

In TypeScript type guards help determine the type of a variable at runtime, they are especially useful when dealing with complex types like unions, discriminated unions or even recursive structures and a recursive type guard is a type guard function that can handle complex nested types, including those that are self-referential.

Prerequisite

What are Recursive Type Guards?

Recursive type guards are functions that are designed to check types for nested or self-referential structures at runtime, these structures might contain recursive relationships such as trees or linked lists where a type references itself.

Why use Recursive Type Guards?

The static type system of TypeScript is strong but sometimes insufficient when dealing with recursive structures and in these cases a recursive type guard helps ensure that each element in a complex structure is correctly typed, even deep into the recursion, this can be useful in data validation or enforcing constraints on complex objects, ensuring runtime safety.

Handling Recursive Types

Recursive types, as mentioned earlier, are types that reference themselves. In TypeScript, you can effectively manage recursive types with various constructs and techniques.

Type Guards

Type Guards are used to narrow down the type of a variable within a conditional block. They help TypeScript understand what type of a variable is at a specific point in code.

Example:

type IsArray<T> = T extends Array<any> ? true : false; 

Mapped Types

Mapped types give you the possibility of generating new types based on the characteristics of other ones, this feature is useful for the recursive types, as they allow for defining manipulations at every depth of recursion.

Example:

type ReadonlyTreeNode<T> = {
readonly [K in keyof T]: ReadonlyTreeNode<T[K]>;
};

Utility Types

Base utility types such as Partial, Required, Record, and so on, can possibly work with the recursive structures to impose limitations or establish new forms, to illustrate, Partial<T>can specify the properties of recursive structure that can be omitted.

Example:

type PartialTreeNode = Partial<TreeNode>;

Conditional Types

Conditional sorts can be used to create kind relationships that rely upon the form of the statistics, they are especially beneficial in recursive scenarios to differentiate between types based totally on homes.

Example:

type IsTreeNode<T> = T extends TreeNode ? true : false;

Recursive Type Guard for a Tree Structure

We want to create a recursive type guard, in order to guarantee that given object complies to TreeNode type. The isTreeNode function first tries to verify that the node is an object that contains a number as value and then it tests whether children property (if it exists) is an array and then goes over child array calling the function isTreeNode again to every child element.

Exanple: This example shows the Recursive Type Guard for a Tree Structure.

JavaScript
/* index.ts */

interface TreeNode {
    value: number;
    children?: TreeNode[];
    // Recursive reference to the same type
}

function isTreeNode(node: any): node is TreeNode {
    if (typeof node !== 'object' || node === null) {
        return false;
    }
    
    if (typeof node.value !== 'number') {
        return false;
    }

    if (node.children) {
        if (!Array.isArray(node.children)) {
            return false;
        }
        
        // Recursively check each child
        for (const child of node.children) {
            if (!isTreeNode(child)) {
                return false;
            }
        }
    }
    
    return true;
}

// Example Usage
const tree = {
    value: 10,
    children: [
        { value: 5 },
        { value: 15, children: [{ value: 12 }, { value: 18 }] }
    ]
};

console.log(isTreeNode(tree));
// Output: true

Output:

true

Recursive Type Guard for JSON-like Object

Lets consider an example here we will implement a recursive type guard for verification if an object follows structure of a JSON object, which can be a string, number, boolean, null, array, or another object. The isJSONValue function is implemented in order to unify all possible usages of a value and check that it corresponds to one of the possible JSON values and in case the value is an array or object, the function traverses each element or each key-value pair to notice whether they coded in JSONValue type.

Exanple: This example shows the Recursive Type Guard for JSON-like Object.

JavaScript
/* index.ts */

type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
interface JSONObject {
    [key: string]: JSONValue;
}
interface JSONArray extends Array<JSONValue> { }

function isJSONValue(value: any): value is JSONValue {
    if (value === null) return true;
    if (typeof value === 'string' || typeof value === 'number'
        || typeof value === 'boolean') {
        return true;
    }

    if (Array.isArray(value)) {
        // Recursively check each item in the array
        return value.every(isJSONValue);
    }

    if (typeof value === 'object') {
        // Recursively check each key-value pair in the object
        for (const key in value) {
            if (value.hasOwnProperty(key) && !isJSONValue(value[key])) {
                return false;
            }
        }
        return true;
    }

    return false;
}

// Example Usage
const jsonObject = {
    name: "Pankaj",
    age: 20,
    married: false,
    children: [
        { name: "Vishal", age: 5 },
        { name: "Vinit", age: 8 }
    ]
};

console.log(isJSONValue(jsonObject));
// Output: true

Output:

true

Recursive Type Guard for a Linked List

Linked list is another recursive data-structure which can be validated using recursive type guards. The method called isListNode determines whether a given node is a ListNode and it then goes on to do same for the next node in a recursive manner, in order to confirm to whether whole link is of ListNode typed structure.

Exanple: This example shows the Recursive Type Guard for a Linked List.

JavaScript
/* index.ts */

interface ListNode {
    value: number;
    next?: ListNode; 
    // Recursive reference to the next node
}

function isListNode(node: any): node is ListNode {
    if (typeof node !== 'object' || node === null) {
        return false;
    }

    if (typeof node.value !== 'number') {
        return false;
    }

    if (node.next !== undefined && node.next !== null) {
        return isListNode(node.next); 
         // Recursive check
    }

    return true;
}

// Example Usage
const linkedList = {
    value: 1,
    next: {
        value: 2,
        next: {
            value: 3
        }
    }
};

console.log(isListNode(linkedList));
// Output: true

Output:

true

Conclusion

Recursive type guards are just well suited for runtime validation of complex and self-referential structures, using recursion and TypeScript's type guards, you should make sure that your data corresponds to the specified structure which in turn increases reliability and reduces possible mistakes on active code, the last illustrative examples covered trees, JSON objects and linked lists are in fact very useful for imaging and subsequent implementation of recursive type guards. This checks if a type is a TreeNode at each recursive level.


Next Article

Similar Reads