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

recursive/nested types for arrays #28929

Closed
ORESoftware opened this issue Dec 9, 2018 · 5 comments · Fixed by #33050
Closed

recursive/nested types for arrays #28929

ORESoftware opened this issue Dec 9, 2018 · 5 comments · Fixed by #33050
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Fix Available A PR has been opened for this issue

Comments

@ORESoftware
Copy link

ORESoftware commented Dec 9, 2018

So I had this:

type NestedRequestHandler = RequestHandler | Array<RequestHandler> | Array<Array<RequestHandler>>

for a certain situation it was actually insufficient and the compiler required me to do this:

type NestedRequestHandler = RequestHandler | Array<RequestHandler> | Array<Array<RequestHandler> | RequestHandler>

I can't figure out why the compiler forced me to do that, the above seems like a bug. Since the second operand in the union type seems like it would have covered it. Am I right or wrong?

Anyway, what I am ultimately looking for is a recursive type for arrays, like so:
https://stackoverflow.com/questions/53646270/declare-arbitrarily-nested-array-recursive-type-definition

@Nathan-Fenner
Copy link
Contributor

Nathan-Fenner commented Dec 10, 2018

Consider the following snippet:

type Nest = number | Array<Nest>;

type Okay = number | { nest: Okay };

Today, the type Nest results in an error:

Type alias 'Nest' circularly references itself.

The type Okay, however, doesn't produce any error.

The current rationale is explained in #12525:

The restriction that a type alias can't be referenced by itself at the top level has been the behavior since we implemented type aliases; however, you might recall that a while back, we started allowing type aliases to be referenced from within an object type.

(emphasis mine)

The current restrictions apply to all type aliases (e.g. type Foo<T> = {x: T} also can't break cycles any more than Array<...> can); you can only "break" cycles by putting things inside object types.

I think a reasonable minimal change to support the above use case would be to add two additional cycle-breakers: Array<T> and T[]. Essentially, since these two types are fixed (i.e. not user-supplied) no analysis needs to be done to ensure that they don't produce infinite types any more than {[x: number]: T} would.

The general case of cycle-breaking in type aliases can be addressed later.

@weswigham weswigham added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Dec 10, 2018
@DanielRosenwasser DanielRosenwasser added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Dec 11, 2018
@DanielRosenwasser
Copy link
Member

Right - type aliases can't be directly recursive because in trying to resolve them, the type-checker would try to eat its own tail and spin off. See more on Wikipedia: https://en.wikipedia.org/w/index.php?title=Recursive_data_type&oldid=812740950#In_type_synonyms

====In type synonyms====
Recursion is not allowed in type synonyms in Miranda, OCaml (unless -rectypes flag is used or it's a record or variant), and Haskell; so for example the following Haskell types are illegal:

type Bad = (Int, Bad)

type Evil = Bool -> Evil

Instead, you must wrap it inside an algebraic data type (even if it only has one constructor):

data Good = Pair Int Good
data Fine = Fun (Bool->Fine)

This is because type synonyms, like typedefs in C, are replaced with their definition at compile time. (Type synonyms are not "real" types; they are just "aliases" for convenience of the programmer.) But if you try to do this with a recursive type, it will loop infinitely because no matter how many times you substitute it, it still refers to itself, e.g. "Bad" will grow indefinitely: (Int, (Int, (Int, ... .

Another way to see it is that a level of indirection (the algebraic data type) is required to allow the isorecursive type system to figure out when to ''roll'' and ''unroll''.

@Nathan-Fenner
Copy link
Contributor

@DanielRosenwasser

We don't need the general case to be handled for this bug to be resolved. Is there any reason that Array<...> can't be given special treatment to resolve this issue (in this particular case)?

@styfle
Copy link
Contributor

styfle commented Apr 22, 2019

Here's a real use case: implementing Array.prototype.flat with any depth.

Right now, lib.esnext.array.d.ts only supports up to depth=7 😮

@ahejlsberg
Copy link
Member

Fixed in #33050.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants
@styfle @DanielRosenwasser @weswigham @ahejlsberg @Nathan-Fenner @ORESoftware and others