DEV Community

Dharan Ganesan
Dharan Ganesan

Posted on

Day 51: Recursive Types

What Are Recursive Types? 🔄

Recursive types in TypeScript allow a type to refer to itself in its definition. This means you can create types that have nested structures, which can be infinitely deep. This concept is particularly handy when dealing with data structures like trees, linked lists, or JSON-like objects.

To illustrate this, let's start with a simple example of a binary tree:

type TreeNode<T> = {
  value: T;
  left?: TreeNode<T>;
  right?: TreeNode<T>;
};
Enter fullscreen mode Exit fullscreen mode

Handling Recursive Types 🧩

Recursive types can sometimes pose challenges, especially when working with deeply nested data. TypeScript offers several techniques to navigate and manipulate these types effectively:

  1. Type Guards: Use type guards like typeof and instanceof to ensure you're dealing with the right type at each level of recursion.

  2. Mapped Types: Leverage mapped types to transform or modify recursive types to suit your needs.

  3. Utility Types: TypeScript provides utility types like Partial, Required, and Record that can be helpful when working with recursive types.

  4. Conditional Types: Use conditional types to apply different type logic based on conditions within the recursive structure.

Examples

1. Modeling Hierarchical Data 🌳

Imagine you're building a file system navigation component, and you want to represent the hierarchical structure of directories and files. Recursive types can help you model this structure effectively:

type FileNode = {
  name: string;
  isFile: boolean;
  children?: FileNode[];
};
Enter fullscreen mode Exit fullscreen mode

2. Parsing JSON Data 🌐

When working with JSON data from external APIs, you often need to parse deeply nested structures. Recursive types make it easier to define the shape of the data you expect:

type JsonValue = string | number | boolean | null | JsonObject | JsonArray;
type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];
Enter fullscreen mode Exit fullscreen mode

3. Implementing Tree Data Structures 🌲

If you're working on algorithms or data structures that involve trees (e.g., binary trees, AVL trees, or B-trees), recursive types are essential for defining the nodes and structures:

type BinaryTreeNode<T> = {
  value: T;
  left?: BinaryTreeNode<T>;
  right?: BinaryTreeNode<T>;
};
Enter fullscreen mode Exit fullscreen mode

Advanced examples with template literal

1. Uppercase a String

You can create a type that transforms a string into uppercase:

type Uppercase<S extends string> = S extends `${infer L}` ? Uppercase<L> : S;

type UppercasedGreeting = Uppercase<"hello">; // UppercasedGreeting = "HELLO"
Enter fullscreen mode Exit fullscreen mode

2. Capitalize the First Letter

Create a type that capitalizes the first letter of a string:

type CapitalizeFirstLetter<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${Uppercase<First>}${Rest}`
  : S;

type CapitalizedGreeting = CapitalizeFirstLetter<"hello">; // CapitalizedGreeting = "Hello"
Enter fullscreen mode Exit fullscreen mode

3. Replace Substrings

Build a type that replaces all occurrences of a substring within a string:

type Replace<S extends string, From extends string, To extends string> = S extends `${infer Prefix}${From}${infer Suffix}`
  ? `${Prefix}${To}${Replace<Suffix, From, To>}`
  : S;

type ReplacedText = Replace<"hello world", "world", "universe">; // ReplacedText = "hello universe"
Enter fullscreen mode Exit fullscreen mode

4. Remove Spaces

Create a type that removes all spaces from a string:

type RemoveSpaces<S extends string> = S extends `${infer Left} ${infer Right}`
  ? RemoveSpaces<`${Left}${Right}`>
  : S;

type NoSpacesText = RemoveSpaces<"hello world">; // NoSpacesText = "helloworld"
Enter fullscreen mode Exit fullscreen mode

5. Reverse a String

Build a type that reverses a string:

type ReverseString<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${ReverseString<Rest>}${First}`
  : S;

type ReversedGreeting = ReverseString<"hello">; // ReversedGreeting = "olleh"
Enter fullscreen mode Exit fullscreen mode

Pitfalls and Considerations 🕳️

While recursive types are a powerful tool, they come with some considerations:

  1. Infinite Types: Be cautious of creating types that could potentially become infinite. TypeScript will eventually enforce a recursion limit.

  2. Performance: Deeply nested recursive types can impact performance, so it's essential to balance type safety with practicality.

  3. Readability: Overly complex recursive types can make your code harder to understand. Use comments and clear type names to enhance readability.

Top comments (1)

Collapse
 
jm__solo profile image
Juan Oliú

Very good article 👏👏.