-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Recursively-typed array with nth-element typing #32967
Comments
@manueliglesias & @mikeparisstuff –– tagging you two in this thread, incase you're interested in watching for the resolution 👍 |
@harrysolovay Not ideal, but this could be a workaround: type Others = 2|3|4|5|6|7|8|9|10|12|13|14|15|16|17|18|19|20; // Up to reasonable number.
type HypertextNodeStructure<T> = unknown[] & {
0: string,
1: Record<string, any>
} & Partial<Record<Others, T | string>>
interface HypertextNode extends HypertextNodeStructure<HypertextNode>{}
let x: HypertextNode = ["div", { id: "" },
["div", {"id": "first-child"}, "I'm the first child"],
["div", {"id": "second-child"}, "I'm the second child"]
]
let d0 = x[0] // string
let d1 = x[1] // Record<string, any>
let d2 = x[2] // string | HypertextNode | undefined |
Thanks @dragomirtitian ... but in the case of long lists (1000+ windowed elements), this wouldn't be good :/ Any other idea? |
interface HypertextNode extends Array<string|Record<string, unknown>|HypertextNode> {
0 : string,
1 : Record<string, unknown>
}
const node : HypertextNode = ["div", {id: "parent"},
["div", {id: "first-child"}, "I'm the first child"],
["div", {id: "second-child"}, "I'm the second child"]
]; |
YESSS. That did the trick. Thank you @AnyhowStep !!! |
Wait- But my approach allows Is that what you actually want? @dragomirtitian 's approach is closer to what you want, even if there's a limit How... Deep can these hierarchies get? [Edit] |
Good catch. It also allows Is it possible to give the right index signatures for type Base<T> = Array<T>;
export interface H extends Base<H> {
0: string;
1: Record<string, unknown>;
} Currently it throws |
@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. @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: export interface H {
0: string;
1: Record<string, unknown>;
[index: not 0 & not 1 & number]: H
} |
@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: export interface H {
0: string;
1: Record<string, unknown>;
[...indices: number[]]: H;
} |
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. |
FYI for everyone here: Tuples are pretty great - |
The recursive part is the only problem... =( |
@weswigham Yeah, we know about those, the problem is the spread at the end can't be type X = [string, Record<string, unknown>, ...X[]] which is ilegal |
You can manually unroll up to some maximum nesting depth, e.g. type HypertextNode =
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string |
[string, Record<string, unknown>, ...Array<string>]>]>]>]>]>]>]>]; Obviously has limitations and I'll readily admit it makes error messages look scary. |
Hi @ahejlsberg –– I'm a big fan of your team's work. Catering to as many use cases as does TypeScript seems difficult (to say the least). I believe this one is worth addressing. My sense is that there are two main reasons to do so:
I'm curious if you see this typing as something that "should" be possible? And if so, do you see it making its way into TypeScript anytime soon? |
@harrysolovay I agree it would be nice to support this somehow. The path of least resistance would probably be to permit type HypertextTuple = [string, Record<string, unknown>, ...HypertextNode[]];
interface HypertextNode extends HypertextTuple {} but this would require some non-trivial work to support extracting rest element types from "tuple-like" types (such as |
@ahejlsberg, thanks for the feedback :) While typing unknown-length tuples would be a cool feature, I doubt it's urgent; please let me know if I should close this issue or leave open. One last (semi-related) note: it could be worthwhile to revisit the self-referencing type discussion, as this would allow for a more intuitive end state for this type: type HypertextNode = [string, Record<string, unknown>, ...HypertextNode[]]; Using an interface for self-referencing gets the job done, but absolutely feels bloated. |
I share the sentiment, but there are much deeper architectural assumptions that would have to be revisited in order to support generalized recursive type aliases. |
🤔 So as it happens, we already broke those assumptions when we added substitutions in conditionals (which are already types-whose-flags-we-dont-know-yet - the substitute could be I think we already started down the path towards having the tools in place to do this, we just haven't acknowledged it yet. |
Being able to do stuff like this would make representing inductive types in TypeScript so much more ergonomic. Right now for an inductive ADT you have to do this: type Node =
| { type: 'identifier', name: string }
| { type: 'assignment', lhs: Node, rhs: Node }
| { type: 'call', target: Node, args: Node[] }; Which is pretty awkward to construct on-demand (object literals aren't exactly compact--especially if you're nesting them) and I really want to be able to do it like this instead: type Node =
| [ 'identifier', string ]
| [ 'assignment', Node, Node ]
| [ 'call', Node, Node[] ]; |
@weswigham Right now substitution types are a narrow mechanism we use to substitute intersection types in place of type parameters. There's quite a bit of ground between that and a general mechanism for deferred type aliases, particularly as type aliases are currently always eagerly resolved. Ultimately I think it means getting rid of the |
Aside: If we're actually assuming the substitute is an intersection anywhere, we're mistaken - a substitute of Anyways, I don't think type IDs meaningfully change - they're just a serializable proxy for a pointer to a (type) object. 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 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 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. |
Let me explain what I mean. In type Rec = [string, Rec?]; we do indeed have a type reference: 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). |
@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 type Defer<T> = T; which is silly, imo. |
@weswigham @RyanCavanaugh See #33050 for a proof of concept implementation of what I discussed above. |
@ahejlsberg @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: class StencilHypertextNode extends Array<
string | Record<string, unknown> | null | StencilHypertextNode
> {
constructor(
tag: string,
props: Record<string, unknown>,
...args: StencilHypertextNode[]
) {
super(...arguments);
this[0] = tag;
this[1] = props;
for (const i in args) {
this[i] = args[i];
}
}
} |
You can't use tuple literals with that, though. const x2 : StencilHypertextNode = [
null, //Expected error
{},
["", {}]
] |
@AnyhowStep true :/ that's too bad. Especially considering the following: function acceptHypertextNode(h: StencilHypertextNode) {
console.log(h);
}
const badInput = [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? |
Recursive type references for array and tuple types are now implemented in #33050. |
Search Terms
• 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:
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: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
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: