-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Allow type aliases to reference themselves in type argument positions #35017
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
Comments
Could you please also add this test case into a test suite for this issue? It's closer to the supposed real-world usage of this pattern. interface A<T> { B: {x: T} }
type C<T extends keyof A<any>, U> = A<U>[T]
type R = C<'B', R> |
I would personally say type Turtle = { down : Turtle }; It's turtles all the way down. I guess this is a valid value. const ouroboros = {} as any;
ouroboros.eating = ouroboros; |
Well, I could have put original code with several mutually recursive union types over several hundred cases, but I think turtles are fine too. If such types were banned, there would be no way to implement Typescript in Typescript, because internal representation of |
My initial reaction was that the recursion has to stop at some point. And that constructing an infinitely recursive type can't really be performed without But I guess there's also, class SelfRecursive {
self : SelfRecursive;
constructor () {
this.self = this;
}
} So, I guess I just wasn't open minded enough =P When I first saw the types in the original post, my head started screaming, "WHERE IS THE EXIT CONDITION!?" |
type B = { x: B }; While this is a legal type, it's worth noting that it's also a Droste type: every inhabitant of this type contains another instance of the same type, and therefore such types can only represent cyclic data (assuming you never lie to the type system). While this doesn't strike me as a particularly useful constraint, it's a perfectly acceptable type with well-defined semantics. On the other hand... type R = A<R>; This is the type that baffles me. In order to know what |
type Level<V, A> = {type: 'node', left: A, right: A} | {type: 'leaf', value: V}
type Annotated<T> = {note: string, value: T}
type Tree<V> = Level<V, Tree<V>>
type AnnotatedTree<V> = Annotated<Level<V, AnnotatedTree<V>>> @fatcerberus Does it make more sense?
|
I think this comment explains the problem: #33050 (comment) Specifically:
Emphasis mine. This seems to be a deliberate tradeoff. In @ahejlsberg's example, he used an interface as the target of the recursive type parameter, not a type alias. |
Writing code on mobile sucks, this is the best I can do, type TreeBase<V, NodeT> =
| { type: 'node', left: NodeT, right: NodeT }
| { type: 'leaf', value: V }
;
type Tree<V> =
| { type: 'node', left: Tree<V>, right: Tree<V> }
| { type: 'leaf', value: V }
;
type AnnotatedTree<V> = {
note: string,
value: TreeBase<V, AnnotatedTree<V>>,
}; |
Stolen from fp-ts, export interface URItoKind<A> {
Tree : Tree<A>,
AnnotatedTree : AnnotatedTree<A>,
}
export type URIS = keyof URItoKind<any>
export type Kind<URI extends URIS, A> = URI extends URIS ? URItoKind<A>[URI] : any
type TreeBase<V, NodeT extends URIS> =
| { type: 'leaf', value: V }
| { type: 'node', left: Kind<NodeT, V>, right: Kind<NodeT, V> }
;
type Tree<V> =
TreeBase<V, "Tree">
;
type AnnotatedTree<V> = {
note: string,
value: TreeBase<V, "AnnotatedTree">,
};
const x : Tree<number> = {
type : "node",
left : { type : "leaf", value : 1337 },
right : {
type : "node",
left : { type : "leaf", value : 9001 },
right : { type : "leaf", value : 69 },
},
}
const y : AnnotatedTree<number> = {
note : "Stolen",
value : {
type : "node",
left : {
note : "From",
value : {
type : "leaf",
value : 3,
},
},
right : {
note : "fp-ts",
value : {
type : "leaf",
value : 4,
},
},
},
} |
Tagging this as a feature request since this has never "worked" in any meaningful sense and it's unclear why it's a strict necessity. |
Funnily enough, this isn’t actually true. I thought the same for a while, but it turns out that There is no way to express the initial instance of such a type as an object literal, however, I’ll freely admit. |
You know how averse someone is to OOP by their initial reaction to |
Sorry to put a description of why I think this is unacceptable behaviour on a more formal side, but I have no idea how to explain better. When we're refactoring code and trying to keep it DRY, it's common to cut a part of a code, make it a function or a type alias, and put a reference to that thing at the call site.
The ability to do so without a hassle in lambda calculus is described by a rule called η-conversion. It literally means that you can convert
this little refactoring shoots my leg off, and I certainly do not expect that. I'm afraid this has something to do with internal represenation of types in TS that keeps track of type aliases for the purposes other than improved error messages. Probably, someone mistakenly used that representation as if type aliases were not ephemeral, but something that does really exist. |
I don't like to be sidetracked with a discussion of a single arbitrary type from an example, but it's already been started, so I have to mention that it's not just circular: it represents lasso structures. |
He probably meant "cyclic" as in, "contains at least one cycle". But I can see how that terminology can be confusing. Just to be sure, I looked it up and Wolfram also says "cyclic" can mean "not acyclic", but is a confusing term outside of graph theory. Since it can be confused with the term "cycle graph". |
Also, regarding η-equivalence, I understand what you mean. It has also been my experience that, even though there are infinite equivalent ways to express a computation at the type level, some ways are more emit-friendly (easier for developers to read in .d.ts files), others are more Instantiation-friendly (letting you compute more complicated and deeply nested types before hitting the depth error), others are more compile-time friendly (taking milliseconds to compile vs seconds/minutes), others just don't work (despite intuition/math saying it should work), and some hit that sweet spot and do exactly what you want, as you want. If you look at my issue history, a lot of my issues basically boil down to,
Most of the time, I don't get an answer =P Without an answer, I just copy-paste the issue link to a comment in my code, and hope I'll understand some day. As an example, once, I had the following, type F<T, U> = G<{
x : T["x"],
y : H<T["y"], U>,
}> And it gave me max depth errors. type F_Impl<X, Y, U> = G<{
x : X,
y : H<Y, U>,
}>
type F<T, U> = F_Impl<T["x"], T["y"], U> Both versions of |
Both issues come from the fact there is something called "instantiation", which doesn't really make sense. Type alias is an alias, there is nothing to create an instance of. |
@AnyhowStep Thank you, this approach literally changes quality of life. I just started marking places with dangerous casts with references to bugs in compiler, and have so much less anxiety by now. |
TypeScript Version: 3.8.0-dev.20191105
Search Terms:
circular
Code
Expected behavior:
Both cases throw no error.
Actual behavior:
Type R is rejected due to a circular reference.
Playground Link:
Link
Related Issues:
#33050
Regression: #33050 (comment)
The text was updated successfully, but these errors were encountered: