46

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?

1

4 Answers 4

52

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> { }
Sign up to request clarification or add additional context in comments.

Comments

41

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'],
];

4 Comments

is there a way to type it a way so it is flattened? Like ['hello', 'w', 'o', 'r', 'l', 'd']
@PEZO do you mean string[]? :) Typescript type definitions don't actually transform values, they just describe their type.
Haha, yes, I know. That's not how I meant. The following does not work: type X<T1> = T1 | X<T2> . I wanted to do something like that: type FullContextRecursive<Child> = GetSelfContext<Child> | FullContextRecursive<GetParentsOf<Child>> . I defined GetParentsOf<T> 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.
Oh, interesting. That sounds like a good candidate for its own stack overflow question
16

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. The JSON data format needs is infinitely recursive, therefore, any generics used to represent the format need to be able to handle an infinite amount of recursion. Any finite representation would be less than adequate.

In older versions of TypeScript (pre v3.7) JSON generics were defined using the following:

    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 cleaner, easier to remember, and is a more accurate abstract representation of the JSON interchange format as defined by ECMA-404.

Comments

9

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

1 Comment

This was merged and the code in the question now works fine.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.