Lesson 78 of 80
A type alias is allowed to reference itself — which is exactly what a comment thread, a file-system tree, or arbitrary JSON needs. TypeScript can follow the recursion; it just won't follow it forever.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Some data is naturally tree-shaped: a comment can have replies, and each reply can have replies of its own, arbitrarily deep. An interface or type alias can reference itself to describe exactly that shape — TypeScript resolves the recursive reference lazily, checking one level at a time rather than trying to expand the type infinitely up front.
This is different from a generic that merely contains another instance of itself once (like a linked-list node with a next field of the same node type) — a recursive type describes structures where the depth genuinely isn't known at the type level, and every level has the exact same shape.
interface Comment {
id: string;
author: string;
body: string;
replies: Comment[]; // Comment referencing itself
}
const thread: Comment = {
id: "1",
author: "ada",
body: "First!",
replies: [
{
id: "2",
author: "grace",
body: "Actually, second.",
replies: [
{ id: "3", author: "ada", body: "Fair.", replies: [] },
],
},
],
};
function countReplies(comment: Comment): number {
return comment.replies.reduce(
(total, reply) => total + 1 + countReplies(reply),
0
);
}
replies: Comment[] is the recursive reference — Comment's definition mentions Comment itself. TypeScript is fine with this as long as the recursion is "productive": each level adds real structure (here, an array) rather than referencing itself directly with nothing in between, which would be circular in a way TypeScript rejects.
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
function stringify(value: JSONValue): string {
return JSON.stringify(value);
}
const config: JSONValue = {
name: "praxis",
tags: ["typescript", "learning"],
nested: { active: true, retries: null },
};
stringify(config); // ✅
stringify(() => {}); // ❌ functions aren't valid JSON
JSONValue is the general-purpose version of the same pattern: a union where two of the branches (the array and the object index signature) refer back to JSONValue itself. This is the actual, precise type for "any value that could come out of JSON.parse" — far more honest than typing it as any, which would silently accept a function or a Date as "valid JSON."
Comment with 10,000 real, deeply-nested replies at runtime is completely fine — the type Comment itself is only ever "one level deep" as far as the compiler is concerned, because replies: Comment[] just refers back to the same already-defined type rather than re-expanding it.JSONValue-shaped unions need every branch to eventually bottom out. string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue } works because string, number, boolean, and null are non-recursive base cases — remove all of those and leave only self-referencing branches, and there'd be no way to ever construct a valid value.countReplies above is an ordinary recursive function; its ability to walk the tree safely (no .replies typo, no assuming a field exists) comes entirely from Comment's recursive type, not from anything special about recursive functions themselves.JSONValue is a discriminated-union-adjacent pattern (from the discriminated unions lesson) taken to its logical extreme — instead of a fixed, finite set of shapes, some of the union's own branches loop back to the union. The "excessively deep" failure mode is the same territory as the infer/conditional-types lesson's more elaborate type-level computations — both push the type checker to do real recursive work, and both have a ceiling worth knowing about before you hit it in a large real codebase.