Repository navigation
Type alias circularly references itself #14174
Description
Activity
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on May 24, 2017 KiaraGrouwstra commented
on Jun 12, 2017 ContributorMore actionsSee the explanation here for more info.
- addedQuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in codeand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Jun 12, 2017 RyanCavanaugh commented
on Jun 12, 2017 MemberMore actionstype Foo = Bar<Foo>
Illegal because we don't know that the definition of
Barisn'ttype Bar<T> = T;Reacted by Chayim Refael Friedman and 见云Reacted by Xie Yuheng and silverwindDon't you have access to the definition of
Barso you could check that?RyanCavanaugh commented
on Jun 12, 2017 MemberMore actionsBarmight betype 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.
Reacted by Aleksi PekkalaCycles 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.
Reacted by Mathieu CAROFF, D Stibbe, Cristian Lozano, rvk220, Michael Sereniti, Sam Vervaeck and silverwindRyanCavanaugh commented
on Jun 12, 2017 MemberMore actionsI 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 workReacted by c69 and Maciek SakrejdaReacted by Michael Sereniti, Nathan Chappell, Izaak Schroeder, Nolan Amy, Ethan Standel and silverwindSure, 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
BarandBazinto the union, substitutingFoofor their type variable.BarandBaz(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.Reacted by Mathieu CAROFF, D Stibbe, Cristian Lozano, Michael Sereniti, Jesse Kelly, Curtis Fenner and MarvinI'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?
Reacted by Marco Manino, Mikaël Mayer, fregante, Mathieu CAROFF, Maciej Bukowski, Daniel Ly, Hugh Collins, Marek Beňovič, Shaked Ofek, Andrew Kaiser and 31 moreReacted by Daniel Ly, Alkis Mavridis, anton-kravchenko, Artur Klesun and MemfisrainAutomatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
Reacted by Bailey Stoner, Matt Stephens, Nathan Baum, TOPKAT, Gili Tzabari, Marvin and silverwindI think this is a reasonable feature request for reasons that are made clear in the comments, can we reopen please?
Reacted by Mathieu CAROFF, Jacob Page, Jacob Weisenburger, Bailey Stoner, Vlad Sirenko, TOPKAT, Gili Tzabari, Jan Wilhelm and Maciek Sakrejda- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Aug 17, 2017 23 remaining items
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.
Reacted by Bob van der Linden and kuzhelovYou 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.
Reacted by Karol Majewski, bb010g, kuzhelov, Colin McDonnell, dvabuzyarov and TOPKATI 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
Arrayinto 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);
Reacted by Safar Ligal and George YongI 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
Reacted by QoVoQ, Jiří Brabec, Maxie, kuzhelov, Xananax, David Schnurr, Kevin, Eugene, Roy Riojas and Paulo CesarI 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.
- addedFix AvailableA PR has been opened for this issueA PR has been opened for this issueand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Aug 30, 2019 With #33050 the original example can now be written as:
interface Bar<A> { x: A } type Foo = Bar<Foo>Reacted by James Conkling, Kevin Beal, kgtkr, Tom Crockett, Donald Pipowitch, SlurpTheo, Paweł Lula, Brandon Chinn, Artem Zakharchenko, Lucas Basquerotto and 8 moreReacted by Luis Herranz, Luke Winship and Sagnik PradhanReacted by Owen Campbell-Mooretype 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:
RebeccaStevens commented
on Jan 27, 2022 More actionsIs 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>];
Reacted by stefnotch and Gajus KuizinasThis 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 }
Reacted by ffxixslhRebecca Stevens (@RebeccaStevens) did you ever figure this out? I asked a related question:
RebeccaStevens commented
on Mar 27, 2023 More actionsNo. The best I came up with was:
export type Alternate<A, B> = | [A] | [A, B, A] | [A, B, A, B, A, ...Array<A | B>];
Reacted by Gajus Kuizinasah, 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[]?

Why does this work:
but this doesn't:
Shouldn't they be equivalent?