38

Let's suppose we try to create HTML build helper

build([
  'html', { lang: 'en' }, [
    ['head', [
      ['title', 'Hello, world!']
    ]
  ]
])

The type declaration for arguments of the build would be (actually it will be more complicated but let's consider just the simplest case)

type Node = [string, { [key: string]: string }, Node[]]

Unfortunately it didn't work, because TypeScript complains

TS2456: Type alias 'Node' circularly references itself.

Is there any workaround?

Humoyun Ahmad
  • 2,875
  • 4
  • 28
  • 46
Alex Craft
  • 13,598
  • 11
  • 69
  • 133
  • 1
    Possible duplicate of [How to create a circularly referenced type in TypeScript?](https://stackoverflow.com/questions/36966444/how-to-create-a-circularly-referenced-type-in-typescript) – keepAlive Dec 16 '17 at 03:33

4 Answers4

43

Type aliases can't be circular, but interfaces can. This accomplishes what you want:

type MyTuple<T> = [string, { [key: string]: string }, T[]];
interface Node extends MyTuple<Node> { }
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
35

This can now be done as of typescript 3.7: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases

For example:

type ValueOrArray<T> = T | ValueOrArray<T>[];
type NestedStringArray = ValueOrArray<string>;

const nestedStringArray: NestedStringArray = [
  'hello',
  ['w', ['o', 'r', 'l'], 'd'],
];
Ulad Kasach
  • 11,558
  • 11
  • 61
  • 87
  • is there a way to type it a way so it is flattened? Like ['hello', 'w', 'o', 'r', 'l', 'd'] – PEZO Aug 24 '21 at 08:45
  • @PEZO do you mean `string[]`? :) Typescript type definitions don't actually transform values, they just describe their type. – Ulad Kasach Aug 25 '21 at 11:00
  • Haha, yes, I know. That's not how I meant. The following does not work: type X = T1 | X . I wanted to do something like that: type FullContextRecursive = GetSelfContext | FullContextRecursive> . I defined GetParentsOf like a database earlier and I wanted to look up items from it. I know it is totally weird, but for my usecase it makes sense. Any ideas? :) Currently I made a workaround and explicitly passed a 16 deep expression instead of recursion. – PEZO Aug 26 '21 at 09:35
  • 2
    Oh, interesting. That sounds like a good candidate for its own stack overflow question – Ulad Kasach Aug 26 '21 at 10:16
9

Soon it will be much easier with Recursive type references https://github.com/microsoft/TypeScript/pull/33050

Daniel Steigerwald
  • 1,075
  • 1
  • 9
  • 19
8

All of the answers here are dated as far as I can tell. In 2022 I just used the following to recursively scan a directory and save all the files and directories returned by the scan:

    type AbstractDirectory = {
      pathname: string;
      files: string[];
    };

    type AbstractFileTree = string | string[] | {
      [ key: string ]: AbstractFileTree;
      dirpath: string;
      filepaths: string[];
    };

Perhaps the most common need for recursive types, is the need to use a true JSON type. JSON is a format that can be as recursive as a programmer needs it to be.

Older Versions of TypeScript (pre v3.7) Typed JSON as Shown Here:

    type Json =  string | number | boolean | 
                 null | JsonObject | JsonArray;

    interface JsonObject {
        [property: string]: Json;
    }

    interface JsonArray extends Array<Json> {}

A good example that was included in the `v3.7 release notes is demonstrated in the following snippet, which is a great solution to any recursive typing that you might be doing.

As of v3.7 or Newer, the Following is Valid TypeScript

type Json =
  | string
  | number
  | boolean
  | null
  | { [property: string]: Json }
  | Json[];

Both examples are recursive, but the later is a much cleaner, more readable, faster to write, easier to remember, and is just flat out a better abstract representation of what JSON is.

JΛYDΞV
  • 8,532
  • 3
  • 51
  • 77