You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Recursively-typed array with nth-element typing #32967
• recursive (tuple|array) of varying length
• differently-typed array elements
• tuple of (generic|unknown) size
Suggestion
Ability to recursively type the elements of varying-length arrays
Use Cases
I'm attempting to create a type for hypertext nodes. Consider the following representation of some HTML:
["div", {"id": "parent"},
["div", {"id": "first-child"}, "I'm the first child"],
["div", {"id": "second-child"}, "I'm the second child"]
]
This is similar to the arguments of React's createElement. While it's possible to type the arguments array of a function and account for rest spread type... this doesn't seem doable in a standalone array. Meanwhile, tuples must be of fixed length. One solution would be to define this hypertext within call expressions:
h("div",{id: "parent"},h("div",{id: "first-child"},"I'm the first child"),h("div",{id: "second-child"},"I'm the second child"),)
However, this format is more bloated. It doesn't convey the most minimal format for the data. My sense is that I should––for the time being––make use of any[].
By the way, apologies if this is currently possible and I just did not find it! Looked for quite a while & no approach seemed to do the trick.
Examples
exporttypeHypertextNodeStructure<T,P,C>=[T,P, ...C[]];exportinterfaceHypertextNodeextendsHypertextNodeStructure<string,{[key: string]: any},HypertextNode>{}consthypertextNode: HypertextNode=["div",{id: "parent"},["div",{id: "first-child"},"I'm the first child"],["div",{id: "second-child"},"I'm the second child"]]
Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript/JavaScript code
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
typeOthers=2|3|4|5|6|7|8|9|10|12|13|14|15|16|17|18|19|20;// Up to reasonable number.typeHypertextNodeStructure<T>=unknown[]&{0: string,1: Record<string,any>}&Partial<Record<Others,T|string>>interfaceHypertextNodeextendsHypertextNodeStructure<HypertextNode>{}letx: HypertextNode=["div",{id: ""},["div",{"id": "first-child"},"I'm the first child"],["div",{"id": "second-child"},"I'm the second child"]]letd0=x[0]// stringletd1=x[1]// Record<string, any>letd2=x[2]// string | HypertextNode | undefined
interfaceHypertextNodeextendsArray<string|Record<string,unknown>|HypertextNode>{0 : string,1 : Record<string,unknown>}constnode : HypertextNode=["div",{id: "parent"},["div",{id: "first-child"},"I'm the first child"],["div",{id: "second-child"},"I'm the second child"]];
[Edit]
You should re-open the issue because I think it's a good feature request.
At least until someone has a workaround that is not too hacky, supports deeply nested tuples, supports arbitrarily long tuples, has the behaviour you want for rest params, etc.
Harry Solovay (@harrysolovay) The limitation here is that an index signature must be consistent with all defined properties, and there is no way to exclude a subset of properties from the index.
Titian Cernicova-Dragomir (@dragomirtitian) that is fantastic! Would that work when extending Arrays? It'd be nice to see a shorthand for rest type as well. Something like this:
In my experience a lot of people already tend to expect the index signature to act as a "rest type" and are surprised when it doesn't, so having that actually in the language would be great.
Reacted by Harry Solovay and Titian Cernicova-Dragomir
Right now substitution types are a narrow mechanism we use to substitute intersection types in place of type parameters.
Aside: If we're actually assuming the substitute is an intersection anywhere, we're mistaken - a substitute of T & U and easily become T & (A | B) which is T & A | T & B. Substitutes are also hiding our already extant ability to have unsimplified unions-within-unions in this way. I think the original intent is maybe slightly different from what they're actually capable of doing today? In any case, I'm only using them as a template since almost every problem one could have with a deferred type alias, we already have with substitutions and are attempting to deal with, so utilizing that seems like a great way to cut down on work.
Anyways, I don't think type IDs meaningfully change - they're just a serializable proxy for a pointer to a (type) object. A | B | C's composite key is still a list of it's component keys, even if any or all of them are substitutes (as it is today).
On type alias vs "type reference types" - the later are, imo, just unfortunately named. They're really just instantiated/declared object types whose members are resolved late... They're better than aliases or deferred substitutes because you can assume they're object types without resolving them (since you looked up that info from the target symbol). I think just renaming them to something like "InstantiatedObjectType" or "LazyObjectType" would be ok.
And while in my super simple example code linked above, I always make a deferred substitute, the cost and potential observable change in behavior can be significantly lowered by only bothering to make one when a circular ref is detected (so if push type would fail (peek type?) return a deferral which doesn't peek (this way if we still need to inspect the type earlier we can still error)).
I've been giving the issue some thought. First I think it is important to identify the exact problem we're trying to solve: Allowing type aliases to be circularly referenced as type arguments in type references. There may well be other constructs in which we could consider permitting circularities, but I specifically want to exclude those here. I think it is key that we understand and scope the problem. If history is any guide we'll otherwise find ourselves chasing infinite recursion stack overflows all over the place.
Currently we eagerly resolve type arguments in type references. This has the advantage type references with identical type arguments end up referencing the same type identity. Technically it wouldn't be that hard to defer resolution of type arguments in type references: Every reference to the typeArguments property would instead become a call to a function that lazily resolves the type arguments. However, we would then have to give every type reference a unique type identity, and two Array<string> references would end up having distinct type objects (just like two distinct { foo: string } object type literals have distinct type objects). The additional type identities would definitely create more work (i.e. more structural type comparisons), though we already have logic in place that efficiently relates two type references to the same generic type and that might actually catch the majority of it.
Regarding using substitution types for circular type aliases, my biggest worry is they give you the ability to introduce circularities anywhere, e.g. in union types, intersection types, indexed access types, etc. We really don't want that. Also, creating them based on peeking the type resolution stack makes it very hard to reason about when they get created.
Actually, the eager resolution of type arguments isn't the issue at all (speaking firsthand here), it's the eager lookup of the declared type of the alias that's the issue, since for aliases today we're forced to resolve it early. (Please note, in type Rec = [string, Rec?], there is no type reference with type arguments (there's a tuple type, which is distinct)) That's how references to aliases are currently different than other type references - type references don't attempt to resolve their declared type until you query their structure.
While peeking the resolution stack is admittedly opaque, I do think it reduces the potential change surface area significantly and also keeps the new type identity count very low. The other option, imo, would be during resolution, saying that a reference to a lexically containing type alias always produces a deferred type, but doing that lexicially is going to heavily affect what works as an alias vs inline (and potentially have visible change to types which are today allowed, like recurring object types), which is much more opaque, imo.
we do indeed have a type reference: [string, Rec?] is simply syntax for a type reference __Txxx__<string, Rec> where __Txxx__ is the synthetic generic interface type we generate for the tuple. So, it's resolved in the same manner as for example Array<Rec> would be resolved. What I'm proposing is that we look at lazily resolving the type arguments in these type references. Specifically, instead of eagerly calling getTypeFromTypeNode for each type argument, wait until someone calls getTypeArguments for the type reference and lazily defer the calls to getTypeFromTypeNode until that point.
Nothing would change with respect to type aliases. I'm specifically not proposing we defer resolution of those (because they can be anything and we don't want all the potential circularities and stack overflows that comes along with that).
Anders Hejlsberg (@ahejlsberg) I don't think doing it at argument positions only makes much sense. It'd be amazingly inconsistent, since you could just defer a thing (anywhere) by wrapping it in a
AnyhowStep true :/ that's too bad. Especially considering the following:
functionacceptHypertextNode(h: StencilHypertextNode){console.log(h);}constbadInput=[null,{},["",{}]];acceptHypertextNode(badInput);// should be a type-error... but there is none
... I'm surprised there doesn't exist a way to define this type of data structure. Any other workaround ideas?
Activity
harrysolovay commentedon Aug 18, 2019
Manuel Iglesias (@manueliglesias) & Michael (@mikeparisstuff) –– tagging you two in this thread, incase you're interested in watching for the resolution 👍
dragomirtitian commentedon Aug 18, 2019
Harry Solovay (@harrysolovay) Not ideal, but this could be a workaround:
Play
harrysolovay commentedon Aug 18, 2019
Thanks Titian Cernicova-Dragomir (@dragomirtitian) ... but in the case of long lists (1000+ windowed elements), this wouldn't be good :/
Any other idea?
AnyhowStep commentedon Aug 18, 2019
Playground
harrysolovay commentedon Aug 18, 2019
YESSS. That did the trick. Thank you AnyhowStep !!!
AnyhowStep commentedon Aug 19, 2019
Wait-
But my approach allows
Record<string, unknown>for elements at index 2,3,4,etc. though.Is that what you actually want?
Titian Cernicova-Dragomir (@dragomirtitian) 's approach is closer to what you want, even if there's a limit
How... Deep can these hierarchies get?
[Edit]
You should re-open the issue because I think it's a good feature request.
At least until someone has a workaround that is not too hacky, supports deeply nested tuples, supports arbitrarily long tuples, has the behaviour you want for rest params, etc.
harrysolovay commentedon Aug 19, 2019
Good catch. It also allows
stringfor those indices :/Is it possible to give the right index signatures for
0and1? This might let us do the following:Currently it throws
Property '1' of type 'Record<string, unknown>' is not assignable to numeric index type 'H'.ts(2412)dragomirtitian commentedon Aug 19, 2019
Harry Solovay (@harrysolovay) The limitation here is that an index signature must be consistent with all defined properties, and there is no way to exclude a subset of properties from the index.
Wesley Wigham (@weswigham) has an interesting combo of PRs that address this negated types and Allow any key type as an index signature parameter type that might allow us to write something like this in the future:
harrysolovay commentedon Aug 19, 2019
Titian Cernicova-Dragomir (@dragomirtitian) that is fantastic! Would that work when extending Arrays? It'd be nice to see a shorthand for rest type as well. Something like this:
fatcerberus commentedon Aug 19, 2019
In my experience a lot of people already tend to expect the index signature to act as a "rest type" and are surprised when it doesn't, so having that actually in the language would be great.
weswigham commentedon Aug 19, 2019
FYI for everyone here: Tuples are pretty great -
[string, Record<string, unknown>, ...number[]]10 remaining items
weswigham commentedon Aug 21, 2019
Aside: If we're actually assuming the substitute is an intersection anywhere, we're mistaken - a substitute of
T & Uand easily becomeT & (A | B)which isT & A | T & B. Substitutes are also hiding our already extant ability to have unsimplified unions-within-unions in this way. I think the original intent is maybe slightly different from what they're actually capable of doing today? In any case, I'm only using them as a template since almost every problem one could have with a deferred type alias, we already have with substitutions and are attempting to deal with, so utilizing that seems like a great way to cut down on work.Anyways, I don't think type IDs meaningfully change - they're just a serializable proxy for a pointer to a (type) object.
A | B | C's composite key is still a list of it's component keys, even if any or all of them are substitutes (as it is today).On type alias vs "type reference types" - the later are, imo, just unfortunately named. They're really just instantiated/declared object types whose members are resolved late... They're better than aliases or deferred substitutes because you can assume they're object types without resolving them (since you looked up that info from the target symbol). I think just renaming them to something like "InstantiatedObjectType" or "LazyObjectType" would be ok.
And while in my super simple example code linked above, I always make a deferred substitute, the cost and potential observable change in behavior can be significantly lowered by only bothering to make one when a circular ref is detected (so if push type would fail (peek type?) return a deferral which doesn't peek (this way if we still need to inspect the type earlier we can still error)).
ahejlsberg commentedon Aug 22, 2019
I've been giving the issue some thought. First I think it is important to identify the exact problem we're trying to solve: Allowing type aliases to be circularly referenced as type arguments in type references. There may well be other constructs in which we could consider permitting circularities, but I specifically want to exclude those here. I think it is key that we understand and scope the problem. If history is any guide we'll otherwise find ourselves chasing infinite recursion stack overflows all over the place.
Currently we eagerly resolve type arguments in type references. This has the advantage type references with identical type arguments end up referencing the same type identity. Technically it wouldn't be that hard to defer resolution of type arguments in type references: Every reference to the
typeArgumentsproperty would instead become a call to a function that lazily resolves the type arguments. However, we would then have to give every type reference a unique type identity, and twoArray<string>references would end up having distinct type objects (just like two distinct{ foo: string }object type literals have distinct type objects). The additional type identities would definitely create more work (i.e. more structural type comparisons), though we already have logic in place that efficiently relates two type references to the same generic type and that might actually catch the majority of it.Regarding using substitution types for circular type aliases, my biggest worry is they give you the ability to introduce circularities anywhere, e.g. in union types, intersection types, indexed access types, etc. We really don't want that. Also, creating them based on peeking the type resolution stack makes it very hard to reason about when they get created.
weswigham commentedon Aug 22, 2019
Actually, the eager resolution of type arguments isn't the issue at all (speaking firsthand here), it's the eager lookup of the declared type of the alias that's the issue, since for aliases today we're forced to resolve it early. (Please note, in
type Rec = [string, Rec?], there is no type reference with type arguments (there's a tuple type, which is distinct)) That's how references to aliases are currently different than other type references - type references don't attempt to resolve their declared type until you query their structure.While peeking the resolution stack is admittedly opaque, I do think it reduces the potential change surface area significantly and also keeps the new type identity count very low. The other option, imo, would be during resolution, saying that a reference to a lexically containing type alias always produces a deferred type, but doing that lexicially is going to heavily affect what works as an alias vs inline (and potentially have visible change to types which are today allowed, like recurring object types), which is much more opaque, imo.
ahejlsberg commentedon Aug 22, 2019
Let me explain what I mean. In
we do indeed have a type reference:
[string, Rec?]is simply syntax for a type reference__Txxx__<string, Rec>where__Txxx__is the synthetic generic interface type we generate for the tuple. So, it's resolved in the same manner as for exampleArray<Rec>would be resolved. What I'm proposing is that we look at lazily resolving the type arguments in these type references. Specifically, instead of eagerly callinggetTypeFromTypeNodefor each type argument, wait until someone callsgetTypeArgumentsfor the type reference and lazily defer the calls togetTypeFromTypeNodeuntil that point.Nothing would change with respect to type aliases. I'm specifically not proposing we defer resolution of those (because they can be anything and we don't want all the potential circularities and stack overflows that comes along with that).
weswigham commentedon Aug 23, 2019
Anders Hejlsberg (@ahejlsberg) I don't think doing it at argument positions only makes much sense. It'd be amazingly inconsistent, since you could just defer a thing (anywhere) by wrapping it in a
which is silly, imo.
ahejlsberg commentedon Aug 23, 2019
Wesley Wigham (@weswigham) Ryan Cavanaugh (@RyanCavanaugh) See #33050 for a proof of concept implementation of what I discussed above.
harrysolovay commentedon Aug 25, 2019
Anders Hejlsberg (@ahejlsberg) Titian Cernicova-Dragomir (@dragomirtitian) AnyhowStep –– I came up with a solution! And I'd love to hear your thoughts on it (do you foresee situations where it might get me into trouble?). Any feedback would be greatly appreciated:
AnyhowStep commentedon Aug 25, 2019
You can't use tuple literals with that, though.
harrysolovay commentedon Aug 25, 2019
AnyhowStep true :/ that's too bad. Especially considering the following:
... I'm surprised there doesn't exist a way to define this type of data structure. Any other workaround ideas?
ahejlsberg commentedon Aug 29, 2019
Recursive type references for array and tuple types are now implemented in #33050.