SyntaxStudy
Sign Up
TypeScript Recursive and Self-Referential Type Aliases
TypeScript Beginner 1 min read

Recursive and Self-Referential Type Aliases

Type aliases can be self-referential, meaning a type can reference itself in its own definition. This is essential for modelling recursive data structures such as trees, linked lists, JSON values, and nested configuration objects. TypeScript handles recursive types without any special syntax — you simply use the type name inside its own definition. JSON is a classic example of a recursive type: a JSON value is either a primitive (string, number, boolean, null) or an array of JSON values or an object whose values are JSON values. Expressing this accurately in TypeScript requires a recursive type alias. Having the correct type for JSON values allows you to parse and traverse JSON data with full type safety. Recursive types can become deeply complex, and TypeScript has limits on how deeply it will instantiate them to prevent infinite recursion in the compiler. When you hit these limits, you often need to refactor to use interfaces (which handle some recursive cases better) or add intermediary types to break the recursion cycle.
Example
// Recursive JSON type
type JSONPrimitive = string | number | boolean | null;
type JSONObject    = { [key: string]: JSONValue };
type JSONArray     = JSONValue[];
type JSONValue     = JSONPrimitive | JSONObject | JSONArray;

const data: JSONValue = {
    name: "Alice",
    scores: [98, 87, 76],
    address: { city: "London", zip: "EC1A 1BB" },
    active: true,
};

// Recursive tree structure
type TreeNode<T> = {
    value: T;
    children: TreeNode<T>[];
};

function sumTree(node: TreeNode<number>): number {
    return node.value + node.children.reduce(
        (acc, child) => acc + sumTree(child),
        0
    );
}

const tree: TreeNode<number> = {
    value: 1,
    children: [
        { value: 2, children: [{ value: 4, children: [] }] },
        { value: 3, children: [] },
    ],
};

console.log(sumTree(tree)); // 10

// Recursive readonly deep type
type DeepReadonly<T> = T extends (infer U)[]
    ? readonly DeepReadonly<U>[]
    : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

type Config = { server: { host: string; port: number }; debug: boolean };
type FrozenConfig = DeepReadonly<Config>;