Advanced TypeScript: Recursive Types
Welcome to the Recursive Types lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
This is our third course on TypeScript. We recommend starting this course after you have at least 1 year of programming experience, and at least 20 hours of experience using TypeScript on your own projects. If you want to start with a more introductory course, consider our Everyday TypeScript course, or maybe even start at the beginning with TypeScript Basics.
Sometimes, a type needs to reference itself. For example, we can define a type for arbitrarily-nested arrays of numbers. Here are some values that it should allow:
1[][1][1, 2, 3][1, 2, [3, 4], 5][[[[1]], 2, [3]]]
Fortunately, declaring this type in TypeScript only requires language features that we've already seen:
>
type NestedNumberArray = number | NestedNumberArray[];NestedNumberArrayis a recursive type. It references itself like a recursive function does. And like a recursive function, that nesting can be resolved as many times as necessary.For example, let's consider
[[[5]]]. What is its type? We can find out by causing an intentional type error, then looking at how TypeScript represents[[[5]]]'s type in the error message.(The code example below causes a type error. When that happens anywhere in this course, you can answer with "type error".)
>
const n: void = [[[5]]];Result:
type error: Type 'number[][][]' is not assignable to type 'void'.
[[[5]]]has the typenumber[][][]. We can get that same type by expanding ourNestedNumberArraytype definition repeatedly. At each step, we'll replaceNestedNumberArraywith either the left or right side of its union definition.- The type is:
type NestedNumberArray = number | NestedNumberArray[] - Repeatedly replace
NestedNumberArray:
NestedNumberArray(Start with the base type.)NestedNumberArray[](Replace theNestedNumberArraywith the right side of the union, which isNestedNumberArray[].)(NestedNumberArray[])[](Replace with the right side of the union again.)((NestedNumberArray[])[])[](And again.)((number[])[])[](ReplaceNestedNumberArraywith the left side of the union, which isnumber.)number[][][](Remove the parentheses.)
- The type is:
Internally, the TypeScript compiler does something similar to that process: it expands the recursive type as many times as necessary.
Let's examine some concrete examples to make sure that they type check. We'll also include some examples that don't type check to make sure that the type rejects incorrect values.
We'll put the string literal
'ok'at the end of each example so you don't have to type long values. If the example compiles, the final line will evaluate to'ok'. For examples that cause type errors, you can answer withtype error.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = 1;'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = 'a';'ok';Result:
type error: Type 'string' is not assignable to type 'NestedNumberArray'.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = {1: 2};'ok';Result:
type error: Type '{ 1: number; }' is not assignable to type 'NestedNumberArray'. Type '{ 1: number; }' is not assignable to type 'number'. - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = [1, 2, 3];'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = [1, 2, [3, 4], 5];'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = [[[[1]], 2, [3]]];'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const n: NestedNumberArray = [[[[1]], 'two', [3]]];'ok';Result:
type error: Type 'string' is not assignable to type 'NestedNumberArray'.
You probably won't need a
NestedNumberArraytype like the one above. But you'll almost certainly encounter JSON. Using recursive types, we can write a single type definition that encompasses all JSON values:>
type Json =| null| boolean| string| number| Json[]| {[key: string]: Json};(The leading
|characters here are a common style for defining multi-line type unions. That union means the same thing asnull | boolean | ...written on one line.)This type combines several type system features. It has a union type. It has an array type,
Json[]. And it has an index signature type,{[key: string]: Json}.Here are a few examples of the
Jsontype in action:- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = {name: 'Amir'};'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = 5;'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = [{name: 'Amir'}, {email: 'betty@example.com'}];'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = [{name: 'Amir', createdAt: new Date()}];'ok';Result:
type error: Type '{ name: string; createdAt: Date; }' is not assignable to type 'Json'. Types of property 'createdAt' are incompatible. Type 'Date' is not assignable to type 'Json | undefined'. Type 'Date' is not assignable to type '{ [key: string]: Json; }'. Index signature for type 'string' is missing in type 'Date'. - Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = [1, [[[2]], 'three', [4]]];'ok';Result:
'ok'
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const json: Json = [undefined];'ok';Result:
type error: Type 'undefined' is not assignable to type 'Json'.
You won't find yourself writing recursive types every day. In fact, it's usually best to avoid them when there's another way. They can be confusing, as shown by some of the error messages above. But sometimes you really do need them, and in those cases you'll be glad that TypeScript supports them!