Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type alias circularly references itself #14174

Closed
pelotom opened this issue Feb 19, 2017 · 42 comments · Fixed by #33050
Closed

Type alias circularly references itself #14174

pelotom opened this issue Feb 19, 2017 · 42 comments · Fixed by #33050
Labels
Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript

Comments

@pelotom
Copy link

pelotom commented Feb 19, 2017

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?

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@KiaraGrouwstra
Copy link
Contributor

See the explanation here for more info.

@RyanCavanaugh RyanCavanaugh added Question An issue which isn't directly actionable in code and removed Needs Investigation This issue needs a team member to investigate its status. labels Jun 12, 2017
@RyanCavanaugh
Copy link
Member

type Foo = Bar<Foo>

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

type Bar<T> = T;

@pelotom
Copy link
Author

pelotom commented Jun 12, 2017

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

@RyanCavanaugh
Copy link
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.

@pelotom
Copy link
Author

pelotom commented Jun 12, 2017

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.

@RyanCavanaugh
Copy link
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

@pelotom
Copy link
Author

pelotom commented Jun 13, 2017

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.

@skreborn
Copy link

skreborn commented Jun 27, 2017

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 @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'
};

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

@mhegazy
Copy link
Contributor

mhegazy commented Aug 17, 2017

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

@mhegazy mhegazy closed this as completed Aug 17, 2017
@pelotom
Copy link
Author

pelotom commented Aug 17, 2017

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

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript and removed Question An issue which isn't directly actionable in code labels Aug 17, 2017
@RyanCavanaugh RyanCavanaugh reopened this Aug 17, 2017
@brandonbloom
Copy link
Contributor

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 }

@zpdDG4gta8XKpMCd
Copy link

please stop bringing this silly "going infinitely deep" excuse, will you? we are all mature developers to know how to deal with it, aren't we?

@zpdDG4gta8XKpMCd
Copy link

@brandonbloom the json problem was solved without recursive types, was posted somewhere in the issues

@RyanCavanaugh
Copy link
Member

please stop bringing this silly "going infinitely deep" excuse, will you? we are all mature developers to know how to deal with it, aren't we?

Definitely 😆

@zpdDG4gta8XKpMCd
Copy link

come on, if you know you going deep you can prevent it, if you dont then you will see those overflows and thrn you know, it is not a stopper by any means or not an excuse

moreover if a problem is so bad maybe it makse sense to add some static checks to see where tge problem is likely to arise?

and why are so few new features to typescript been made lately, are you guys on a cut budget?

@RyanCavanaugh
Copy link
Member

Lots of summer vacations, some parental leave, slightly more staff turnover than usual, and mapped types had a substantial bug tail that's taken a lot of time.

@pelotom
Copy link
Author

pelotom commented Sep 7, 2017

I appreciate the TypeScript team's hard work. Mapped types have been a great addition and well worth the added complexity and corner cases!

@thorn0
Copy link

thorn0 commented May 17, 2018

Is it the same issue? playground

// Error: Type alias 'DeepPartial' circularly references itself
type DeepPartial<T> = T extends Array<infer U>
  ? Array<DeepPartial<U>>
  : T extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : T extends Date
      ? T
      : { [P in keyof T]?: DeepPartial<T[P]> };

upd: found working DeepPartial here

@donaldpipowitch
Copy link
Contributor

donaldpipowitch commented Jun 18, 2018

I also wonder if I hit this bug or if it is different (modified example from this Tweet):

type FormValue<T> = { value: T }; // potentially many more fields

type FormModel<T> =
    //T extends Array<infer U> ? { value: FormModel<U>[] } : // THIS WORKS
    T extends Array<infer U> ? FormValue<FormModel<U>[]> :   // THIS ERRORS
    T extends object ? FormValue<FormModelObject<T>> :
    FormValue<T>;

type FormModelObject<T> = {
    [key in keyof T]: FormModel<T[key]>;
};

const model: FormModel<{
    bar: string,
    baz: {
        foo: string
    },
    items: string[]
}> = {} as any;

model.value.bar.value.toLowerCase();
model.value.baz.value.foo.value.toLowerCase();
model.value.items.value.map(item => item.value.toLowerCase());

(Playground)

I wonder if there is a solution were I can re-use FormValue for the array-case instead of re-writing it...?


Update: Looks like I found a solution in the DeepPartial comment from above. See this new example. Works, but was hard to figure out.

@kgtkr kgtkr mentioned this issue Aug 7, 2018
4 tasks
@babakness
Copy link

@tycho01 The code quoted by @Aleksey-Bykov is brilliant and it works; but only the first time. If I change the value of one of the tuples being concatenated, TypeScript hangs.

Also adding this function to the bottom of the concat example will hang Typescript

declare function foo<A, B>(a: A[], b: B[]): Concat<A,B>

Example link:

http://www.typescriptlang.org/play/#src=%0D%0Atype%20ToTuple%3CT%3E%20%3D%20T%20extends%20any%5B%5D%20%3F%20T%20%3A%20any%5B%5D%3B%0D%0A%0D%0Atype%20Head%3CTuple%20extends%20any%5B%5D%3E%20%3D%20Tuple%20extends%20%5Binfer%20H%2C%20...any%5B%5D%5D%20%3F%20H%20%3A%20never%3B%0D%0Atype%20Tail%3CTuple%20extends%20any%5B%5D%3E%20%3D%20((...t%3A%20Tuple)%20%3D%3E%20void)%20extends%20((h%3A%20any%2C%20...rest%3A%20infer%20R)%20%3D%3E%20void)%20%3F%20R%20%3A%20never%3B%0D%0Atype%20Unshift%3CTuple%20extends%20any%5B%5D%2C%20Element%3E%20%3D%20((h%3A%20Element%2C%20...t%3A%20Tuple)%20%3D%3E%20void)%20extends%20(...t%3A%20infer%20R)%20%3D%3E%20void%20%3F%20R%20%3A%20never%3B%0D%0Atype%20Push%3CTuple%20extends%20any%5B%5D%2C%20Element%2C%20R%20%3D%20Reverse%3CTuple%3E%2C%20T%20extends%20any%5B%5D%3D%20ToTuple%3CR%3E%3E%20%3D%20Reverse%3CUnshift%3CT%2C%20Element%3E%3E%3B%0D%0A%0D%0Atype%20Reverse%3CTuple%20extends%20any%5B%5D%3E%20%3D%20Reverse_%3CTuple%2C%20%5B%5D%3E%3B%0D%0Atype%20Reverse_%3CTuple%20extends%20any%5B%5D%2C%20Result%20extends%20any%5B%5D%3E%20%3D%20%7B%0D%0A%20%20%20%201%3A%20Result%2C%0D%0A%20%20%20%200%3A%20Reverse_%3CTail%3CTuple%3E%2C%20Unshift%3CResult%2C%20Head%3CTuple%3E%3E%3E%0D%0A%7D%5BTuple%20extends%20%5B%5D%20%3F%201%20%3A%200%5D%3B%0D%0A%0D%0Atype%20Concat%3CTuple1%20extends%20any%5B%5D%2C%20Tuple2%20extends%20any%5B%5D%2C%20R%20%3D%20Reverse%3CTuple1%3E%2C%20T%20extends%20any%5B%5D%3D%20ToTuple%3CR%3E%3E%20%3D%20Concat_%3CT%2C%20Tuple2%3E%3B%0D%0Atype%20Concat_%3CTuple1%20extends%20any%5B%5D%2C%20Tuple2%20extends%20any%5B%5D%3E%20%3D%20%7B%0D%0A%20%20%20%201%3A%20Reverse%3CTuple1%3E%2C%0D%0A%20%20%20%200%3A%20Concat_%3CUnshift%3CTuple1%2C%20Head%3CTuple2%3E%3E%2C%20Tail%3CTuple2%3E%3E%2C%0D%0A%7D%5BTuple2%20extends%20%5B%5D%20%3F%201%20%3A%200%5D%3B%0D%0A%0D%0Atype%20x%20%3D%20Concat%3C%5B1%2C%202%2C%203%5D%2C%20%5B4%2C%205%2C%206%5D%3E%3B%20%2F%2F%20%5B1%2C%202%2C%203%2C%204%2C%205%2C%206%5D%0D%0Atype%20y%20%3D%20Concat%3C%5B1%2C%202%2C%203%2C%204%5D%2C%20%5B5%2C%206%2C%207%5D%3E%3B%20%2F%2F%20%5B1%2C%202%2C%203%2C%204%2C%205%2C%206%5D%0D%0A%0D%0Adeclare%20function%20foo%3CA%2C%20B%3E(a%3A%20A%5B%5D%2C%20b%3A%20B%5B%5D)%3A%20Concat%3CA%2CB%3E

@bobvanderlinden
Copy link

Hmm, I think I also ran into this issue when trying to implement a type that deeply removes | null and optionalField?s from GraphQL-like structures:

type GraphQLObject = { '__typename': string }
type DeepRequired<T> = NonNullable<
  T extends Array<infer TItem>
    ? Array<DeepRequired<TItem>>
    : T extends GraphQLObject
      ? Required<{ [TKey in keyof T]: DeepRequired<T[TKey]> }>
      : T
>

The hack mentioned earlier will not work for me (or I haven't figured it out yet), because I need to infer TItem.

@skreborn
Copy link

skreborn commented Nov 24, 2018

@bobvanderlinden Would the following satisfy your requirements?

type GraphQLObject = { '__typename': string }

type DeepRequired<T> = T extends GraphQLObject ?
    Required<{ [TKey in keyof T]: DeepRequiredObject<T[TKey]> }> :
    (T extends Array<infer TItem> ? DeepRequiredArray<TItem> : DeepRequiredObject<T>);

interface DeepRequiredArray<T> extends Array<DeepRequired<T>> {}

type DeepRequiredObject<T> = {
    readonly [K in keyof T]: DeepRequired<T[K]>;
}

It seems to be producing the correct result in a simple case, but I'm unsure if it works in all required scenarios as I haven't yet used GraphQL myself.

interface Result {
    data: {
        search: [
            {
                __typename: string,
                name: string | null
            }
        ]
    }
}

const result: DeepRequired<Result> = {
    data: {
        search: [
            {
                __typename: "Human",
                name: "Han Solo"
            },
            {
                __typename: "Human",
                name: "Leia Organa"
            },
            {
                __typename: "Starship",
                name: "TIE Advanced x1"
            }
        ]
    }
};

result.data.search[0].name; // string

The implementation is largely based on @nieltg's DeepImmutable.

@bobvanderlinden
Copy link

@skreborn Wow, thanks for that one 😁👍. It's still a bit confusing why this variant does work, but directly inlining DeepRequiredObject and DeepRequiredArray doesn't. Is such a solution considered a workaround for this issue? Or am I just doing something fundamentally wrong in my type definition?

@skreborn
Copy link

@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.

@babakness
Copy link

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.

@pronebird
Copy link

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);

@flux627
Copy link

flux627 commented Aug 7, 2019

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

@kevinbeal
Copy link

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.

@ahejlsberg ahejlsberg added Fix Available A PR has been opened for this issue and removed In Discussion Not yet reached consensus labels Aug 30, 2019
@ahejlsberg
Copy link
Member

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

interface Bar<A> { x: A }
type Foo = Bar<Foo>

@amatiasq
Copy link

amatiasq commented Jun 8, 2021

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

@RebeccaStevens
Copy link

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

@zaydek
Copy link

zaydek commented Mar 16, 2022

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 }

@gajus
Copy link

gajus commented Mar 27, 2023

@RebeccaStevens did you ever figure this out? I asked a related question:

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

@RebeccaStevens
Copy link

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

@gajus
Copy link

gajus commented Mar 27, 2023

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
Labels
Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.