Skip to content

Type alias circularly references itself #14174

Description

@pelotom

Why does this work:

type Foo = { x: Foo }

but this doesn't:

type Bar<A> = { x: A }
type Foo = Bar<Foo>
//   ^^^ Type alias 'Foo' circularly references itself

Shouldn't they be equivalent?

Activity

  1. KiaraGrouwstra commented on Jun 12, 2017

    @KiaraGrouwstra
    Contributor

    See the explanation here for more info.

  2. added
    QuestionAn issue which isn't directly actionable in code
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Jun 12, 2017
  3. RyanCavanaugh commented on Jun 12, 2017

    @RyanCavanaugh
    Member
    type Foo = Bar<Foo>

    Illegal because we don't know that the definition of Bar isn't

    type Bar<T> = T;
    
  4. pelotom commented on Jun 12, 2017

    @pelotom
    Author

    Don't you have access to the definition of Bar so you could check that?

  5. RyanCavanaugh commented on Jun 12, 2017

    @RyanCavanaugh
    Member

    Bar might be

    type Bar<T> = Foo<T>;
    

    and now we're going infinitely deep.

    Type aliases and interfaces are subtly different and there are rules about self-recursion for aliases that don't apply to interfaces. Because an alias is supposed to always be "immediately expandable" (it's as if it were an in-place expansion of its referand), there are things you can do interfaces you can't do with type aliases.

  6. pelotom commented on Jun 12, 2017

    @pelotom
    Author

    Cycles can be detected and you can give up at that point, but as long as you can topologically order the type aliases by their references to one another it's fine. Haskell, PureScript, Scala etc have no problem with this.

  7. RyanCavanaugh commented on Jun 12, 2017

    @RyanCavanaugh
    Member

    I mean, we could, it's just that writing interface Bar<T> extends Foo<T> { } accomplishes the same thing without requiring us to rearchitect how type aliases work

  8. pelotom commented on Jun 13, 2017

    @pelotom
    Author

    Sure, you can do that to emulate

    type Foo = Bar<Foo>

    But what about

    type Foo = Bar<Foo> | Baz<Foo>

    In this situation afaict the only solution is to expand the definitions of Bar and Baz into the union, substituting Foo for their type variable. Bar and Baz (and however many other alternatives) could be large complex interfaces, and must now each be maintained as 2 separate copies that must be kept in sync with one another.

  9. skreborn commented on Jun 27, 2017

    @skreborn

    I'm trying to create a type that circularly references itself, but in a way that it makes sense - or at least it does to me. This is basically the same problem as Tom Crockett (@pelotom) is having.

    type Data = number | string | Data[] | Record<string, Data>;

    If I modify it to the following, it works as intended, but this really is just more work for the user. This gets even worse when the data types are more complicated than a simple key-value pair.

    type Data = number | string | { [key: number]: Data } | { [key: string]: Data };

    This type allows me to create values like the following and handle them in a type safe way even when they are deeply nested.

    const data: Data = {
        foo: [ 1, 'one', { two: 2 } ],
        bar: 'foobar'
    };

    Ryan Cavanaugh (@RyanCavanaugh) Is there any other (sane) way to create type-safe nested objects?

  10. mhegazy commented on Aug 17, 2017

    @mhegazy
    Contributor

    Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

  11. pelotom commented on Aug 17, 2017

    @pelotom
    Author

    I think this is a reasonable feature request for reasons that are made clear in the comments, can we reopen please?

  12. 23 remaining items

  13. skreborn commented on Nov 24, 2018

    @skreborn

    Bob van der Linden (@bobvanderlinden) This is, as far as I know, the only workaround for now. Inlining them doesn't work because circular referencing is not allowed. So why is it allowed if you put an extra step between the two? It's black magic that might have something to do with lazy evaluation, but it's definitely beyond my knowledge on the internal workings of the type system.

  14. babakness commented on Nov 24, 2018

    @babakness

    You can sort of induce lazy evaluation by using the following pattern

    type DeepUnnest<P extends any[]> = {
      'more': P extends Array<infer U>
        ?  U extends any[]
           ? DeepUnnest<U>
           : never
        : never
      'end': P extends Array<infer U>
        ? U
        : never,
    }[
      P extends Array<infer U>
       ? U extends any[]
         ? 'more'
         : 'end'
       : 'end'
    ]

    Its ugly but it works avoiding circular reference problems.

  15. pronebird commented on Feb 28, 2019

    @pronebird

    I found that cyclic references work very well with nested objects

    class Node<T> {}
    
    type ExtractType<T> =
        T extends SchemaNode<infer S>
            ? S extends { [name: string]: SchemaNode<infer _> } 
                ? { [K in keyof S]: ExtractType<S[K]> }
                : S extends Array<infer E> 
                    ? any[]
                    : S           
        : T;

    But as soon as I add an Array into the mix and attempt to type it, the whole thing breaks, i.e:

    class Node<T> {}
    
    type ExtractType<T> =
        T extends SchemaNode<infer S>
            ? S extends { [name: string]: SchemaNode<infer _> } 
                ? { [K in keyof S]: ExtractType<S[K]> }
                : S extends Array<infer E> 
                    ? Array<ExtractType<E>> 
                    : S           
        : T;

    Full source:

    class SchemaNode<T> {
        constructor(private value: T) { }
        validate(): ExtractType<SchemaNode<T>> {
            return this.value as any; // [redacted]
        }
    }
    
    type ExtractType<T> =
        T extends SchemaNode<infer S>
            ? S extends { [name: string]: SchemaNode<infer _> } 
                ? { [K in keyof S]: ExtractType<S[K]> }
                : S extends Array<infer E> 
                    ? Array<ExtractType<E>>
                    : S           
        : T;
    
    type AccountType = "retail" | "company";
    
    const mySchema = new SchemaNode({
        account: new SchemaNode({
            token: new SchemaNode("john doe"),
            type: new SchemaNode<AccountType>("retail"),
            options: new SchemaNode({
                autoLogin: new SchemaNode(true)
            }),
            isActive: new SchemaNode(true),
        }),
        history: new SchemaNode([
            new SchemaNode("went to barber shop")
        ]),
        credits: new SchemaNode(1000)
    });
    
    const res = mySchema.validate();
    
    (res.account.token as string);
    (res.account.isActive as boolean);
    (res.account.type as AccountType);
    (res.account.options.autoLogin as boolean);
    (res.history as string[]);
    (res.credits as number);
  16. flux627 commented on Aug 7, 2019

    @flux627

    I think aleksey-bykov nailed the primary goal use case in this comment:
    #6230 (comment)

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

    Here's my work-around:

    type JsonPrimitive = string | number | boolean | null
    interface JsonMap { [member: string]: JsonPrimitive | JsonArray | JsonMap }
    interface JsonArray extends Array<JsonPrimitive | JsonArray | JsonMap> {}
    type Json = JsonPrimitive | JsonMap | JsonArray
  17. kevinbeal commented on Aug 14, 2019

    @kevinbeal

    I wanted to do this:

    type lineOfCode = [string, lineOfCode[], [string, void | number]];

    But I had to do this workaround:

    type _lineOfCode = [string, unknown, [string, void | number]];
    interface ILineOfCode extends _lineOfCode {
    	1: ILineOfCode[];
    }

    Maybe this will help someone else.

  18. ahejlsberg commented on Aug 30, 2019

    @ahejlsberg
    Member

    With #33050 the original example can now be written as:

    interface Bar<A> { x: A }
    type Foo = Bar<Foo>
    
  19. amatiasq commented on Jun 8, 2021

    @amatiasq
    type JsonPrimitive = string | number | boolean | null
    interface JsonMap { [member: string]: JsonPrimitive | JsonArray | JsonMap }
    interface JsonArray extends Array<JsonPrimitive | JsonArray | JsonMap> {}
    type Json = JsonPrimitive | JsonMap | JsonArray

    Maybe that worked in a previous version but I'm getting this:

    Screenshot 2021-06-08 at 16 20 31

  20. RebeccaStevens commented on Jan 27, 2022

    @RebeccaStevens

    Is it possible to write a tuple type that repeats a pattern?

    // [A] or [A, B, A] or [A, B, A, B, A] or [A, B, A, B, A, B, A] or ...
    type PatternTuple<A, B> = [A] | [A, B, ...PatternTuple<A, B>];
  21. zaydek commented on Mar 16, 2022

    @zaydek

    This is working fine for me so far. *crosses fingers*

    type JSONValue =
      | null
      | boolean
      | number
      | string
    
    type JSONArray =
      | JSONValue[]
      | JSONArray[]
      | JSONMap[]
    
    // Use interface because type errors:
    //
    //   type JSONMap = Record<string, JSONValue | JSONArray | JSONMap>
    //   -> Type alias 'JSONMap' circularly references itself. ts(2456)
    //
    interface JSONMap { [key: string]: | JSONValue | JSONArray | JSONMap }
  22. gajus commented on Mar 27, 2023

    @gajus

    Rebecca Stevens (@RebeccaStevens) did you ever figure this out? I asked a related question:

    https://stackoverflow.com/q/75851865/368691

  23. RebeccaStevens commented on Mar 27, 2023

    @RebeccaStevens

    No. The best I came up with was:

    export type Alternate<A, B> =
      | [A]
      | [A, B, A]
      | [A, B, A, B, A, ...Array<A | B>];
  24. gajus commented on Mar 27, 2023

    @gajus

    ah, I missed [] in my question/example.

    type NodeSnapshot =
      | string
      | [string, Attributes, ...NodeSnapshot]

    The above should have been:

    type NodeSnapshot =
      | string
      | [string, Attributes, ...NodeSnapshot[]]

    The circular reference error threw me off.

    Maybe it would make sense to change the error to:

    - Type alias 'PlaywrightNodeSnapshot' circularly references itself.
    + Type alias 'PlaywrightNodeSnapshot' circularly references itself. Did you mean to spread NodeSnapshot[]?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fix AvailableA PR has been opened for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions