Mastering Recursive Type Aliases in TypeScript

Mastering Recursive Type Aliases in TypeScript

Sunny Sun Lv4

** The post was updated on 28-07-2024

Recursive types in TypeScript are a feature that allows a type to reference itself in its definition. This capability is particularly useful for modeling structures that naturally recur, such as trees, linked lists, and other hierarchical data structures.
In this article, I will explore how to use recursive type aliases in TypeScript.

A bit of history

Before TypeScript 3.7, a recursive type reference will cause the TypeScript compiler to throw a circular references error message. Developers must make a workaround (i.e., using an interface) to achieve a recursive reference.

The recursive type has been introduced since TypeScript 3.7. It allows the reference to the type from its own definition by deferring the type reference until the instantiating .

As recursion is a common programming pattern, there are many use cases in which a recursive type is very useful. Using recursive type, we can concisely represent a complex data structure.

For example, consider a tree structure where each node can have children that are also of the same type. With the introduction of recursive types, this is now possible by deferring the evaluation of the type reference.

1
2
3
4
5
type TreeNode = {
value: number;
children?: TreeNode[]; // The type can reference itself here.
};

Let’s explore some practical use cases.

Practical use of recursive type

Recursive type aliases find application in various scenarios:

  • Tree-like Structures: Modeling hierarchical data like file systems, organizational charts, or DOM structures.
    & Linked Lists: Representing linked lists with nodes pointing to the next element.
  • Graph Structures: Creating graph data structures with nodes and edges.
  • Cyclic Data Structures: Defining JSON-like structures with potential circular references.

The recursive type can be used to represent a data type that has a nested structure. Below is an example:

1
2
3
4
5
6
7
8
9
10
const myData = {  
top: 1,
rest: {
top: 2,
rest: {
top: 3,
rest: null
}
}
};

This data type can be abstracted as a stack data type.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Stack<T> = {  
top: T;
rest: Stack<T> | null;
} ;

// Then we can strong typed the data
const myData: Stack<number> = {
top: 1,
rest: {
top: 2,
rest: {
top: 3,
rest: null
}
}
};

The above code defines a generic data type Stack. It is composed of a top property of type T, and a rest property that is of the same Stack type inside the type definition.

Another good example is the JSON data type. In the recursive type TypeScript playground example, the following code snippet is used to define a JSON type:

1
2
3
4
5
6
7
8
9
10
11
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };  

const exampleStatusJSON: Json = {
available: true,
username: "Jean-loup",
room: {
name: "Highcrest",
// Cannot add functions into the Json type
// update: () => {}
},
};

The Json type works just like our previous Stack type, and we are using the Json type alias to represent the nested JSON child nodes.

As shown in the above examples, using recursive type makes the type definition cleaner and more readable, as the type definition matches the recursive nature of a data structure.

Limitation of Recursive Type

The recursive type aliases in TypeScript have the limitation of not allowing immediate “self-instantiation”. Below is an example:

1
2
3
4
5
6
type Stack<T> = {  
top: T;
rest: Stack<T> | null;
} ;

type Stack1 = Stack<Stack1>;

In this case, the TypeScript compiler throws an error: “_Type alias ‘Stack1’ circularly references itself. (2456)_”. The restriction is reasonable, as the immediate self-reference in the above example will cause infinity recursion in compile time.

Advanced Usage

We can achieve some complex type operations by combining recursive type aliases with other advanced type features (i.e., Conditional Type).

Let’s say we have a Client Type that represents client data.

1
2
3
4
5
6
7
8
9
10
type Client = {  
id: number,
name: string,
address: {
id: number,
suburb: {
postCode: number
}
}
};

We aim to define a PropertyType property type that can extract from the nested type structure.

1
2
3
4
type postCode = PropertyType<Client, 'address.suburb.postCode'>;  
// the postCode type returns "number"
type noExist = PropertyType<Client, 'address.suburb.noExist'>;
// return "never" because the path does not exist

The type should be able to take two arguments, a generic type argument, and a property path string, to locate the property. If the property path doesn’t exist, then return thenever type.

Recursive type aliases fit this situation well, enabling us to traverse through the object type structure.

Below is the implementation of the PropertyType

1
2
3
4
5
type PropertyType<T, Path extends string> =  
Path extends keyof T ? T[Path] :
Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropertyType<T[K], R> :
never :
never;

Although the type is just a one-liner, it makes use of a few advanced type features on top of recursive type aliases:

The first part of the conditional type checks whether the path is a key of type T, if it is, then the type is set to be the key value: T[Path].

1
Path extends keyof T ? T[Path]

Then, infer operator is used in the second part of the conditional type check to extract K and R out. Here, a pattern ${...}.${...} is used to match the string with . exists.

1
Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropertyType<T[K], R>

When all conditions match, we make a recursive call to the next level property PropertyType<T[K], R>. Otherwise, it means the Path doesn’t exist, and a never type will be returned.

Challenges and Considerations

While recursive type aliases are powerful, they also introduce potential complexities:

  • Type Inference Limitations: TypeScript’s type inference might struggle with complex recursive types in certain scenarios. we might need to provide explicit type annotations for clarity.
  • Performance Implications: Large recursive structures can impact performance. Consider using techniques like memoization or structural sharing to optimize memory usage.

Conclusion

In this article, we examine a few recursive type aliases. Recursive type alias can represent a data structure with a recursion nature, and we can also use it to traverse or manipulate complex data types.

I hope this article can be useful to you. Happy programming!

  • Title: Mastering Recursive Type Aliases in TypeScript
  • Author: Sunny Sun
  • Created at : 2023-05-01 00:00:00
  • Updated at : 2024-07-28 10:27:56
  • Link: http://coffeethinkcode.com/2023/05/01/mastering-recursive-type-alias/
  • License: This work is licensed under CC BY-NC-SA 4.0.