Repository navigation
Description
🔎 Search Terms
Instantiation expression, referenced directly or indirectly in its own type annotation, circular type annotation, this, parent class base constraint.
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about referenced directly or directly in its own type annotation
⏯ Playground Link
💻 Code
class WitnessIsParentClass<const out SomeClass extends ParentClass> {}
class ParentClass {
someWitness(witness: typeof WitnessIsParentClass<this>): void {}
}
class SubClass extends ParentClass {
someWitness(witness: typeof WitnessIsParentClass<this>): void { }
// ^ 'witness' is referenced directly or indirectly in its own type annotation.(2502)
}🙁 Actual behavior
This errors with 'witness' is referenced directly or indirectly in its own type annotation.(2502)
🙂 Expected behavior
No error.
Additional information about the issue
If you change from typeof WitnessIsParentClass<this> to WitnessIsParentClass<this> the error goes away so this seems to be specific to instantiation expressions. I was hoping the variance annotation would give it enough information to resolve this problem but it didn't. I suppose that's probably because the variance annotations are only for the instance but not for the class as a whole.
This code example is obviously contrived but I ran into this while writing a complex mixin. I've actually ran into this style of error; 'x' is referenced directly or indirectly in its own type annotation.(2502) many times but this was the first time it was so easy to create a minimal example. I think the root cause in those cases may have been different in those cases however, so I should probably find a way to simplify them enough to share.
Interestingly if you add a level of indirection this works fine:
class WitnessIsParentClass<SomeClass extends ParentClass> {}
type WitnessHelper<SomeClass extends ParentClass> = typeof WitnessIsParentClass<SomeClass>
class ParentClass {
someWitness(witness: WitnessHelper<this>): void {}
}
class SubClass extends ParentClass {
someWitness(witness: WitnessHelper<this>): void { }
}I assume this is because it essentially hoists the variance check and breaks the loop or something.
Activity
RyanCavanaugh commentedon Feb 8, 2024
This is a true circularity:
SubClasshas to be assignable toParentClassfor the declaration to be legalSubClassassignable toParentClass?SubClass#someWitnessis a subtype ofParentClass#someWitnesswitnessparameterSubClass-restricted instantiation ofWitnessHelper<this>is assignable to aParentClass-restricted instantiation ofWitnessHelper<this>WitnessHelper<subtype of SubClass>is assignable toWitnessHelper<subtype of ParentClass>SubClassis assignable toParentClassSubClassassignable toParentClass?DavidArchibald commentedon Feb 8, 2024
Ryan Cavanaugh (@RyanCavanaugh), thank you for the explanation, however I'm confused because you mention
WitnessHelperin your explanation. However in the code example where I usedWitnessHelperit compiles and functions seemingly perfectly, that's what I meant by the line right before the code snippet that brought inWitnessHelper:I guess maybe in that case it's truely circular in a broken way too but there's simply no compile error? That actually seems worse though and should be fixed.
To clarify the original code snippet is the only code snippet I shared that doesn't compile. That'd be the one under "Playground Link" and "Code" but the one I mention in "Additional information about the issue" compiles which was meant as a clarification about how it doesn't seem impossible to solve.
Here's some more code snippets that also compiles:
(The implementation options are mutually exclusive)
And in general patterns like this compile:
So I'm unfortunately not following the explanation because my gut instinct is to say that the same about it truly being explanation applies. Especially since
WitnessHelperonly introduces a bit of indirection and then eventypeof WitnessIsParentClass<SomeClass>where the inlined version doesn't.RyanCavanaugh commentedon Feb 9, 2024
It's not broken. Basically, there are places where deferrals can occur, so if you introduce a deferral at one of the steps in the process, then the question at the bottom of the list gets asked at a later time where the question at the top of the list isn't one under consideration, removing the circularity.
But not every operation is deferred (this is bad for both performance and, in some cases, semantics), so two programs that appear equivalent actually have different evaluation semantics in terms of when certain types get resolved. We're always working to try to reduce the number of circularity errors you encounter (e.g. #57293 this week) but it's not feasible to reduce them all the way to zero, nor is it desirable to error more than needed in order to achieve greater consistency.
DavidArchibald commentedon Feb 9, 2024
Thank you for elaborating! I've been interested in the codebase so I could be willing to contribute a fix... if I eventually get the know how. Unfortunately I've found running through the core logic of the checker to be a large hurdle and even just opening the TypeScript repo can slow down VSCode quite a bit so it's been difficult.
Besides that, I would say one major thing; the error message
'witness' is referenced directly or indirectly in its own type annotation.(2502)is rather undescriptive of the circular error at hand.I'm sure there's some root reasons why it'd be tricky to improve but if you'd indulge me, I'd suggest something a bit like
'witness''s type annotation leads to a circular loop. I've also seen existing errors that would be better to have been emitted in the ideal case. Because it's not like I wrotewitness: typeof witness(a case where this emitted case makes a lot of sense).Given that I know how to fix this (by deferring with
WitnessHelper) and since I don't really personally need action so I can close this if you'd like but I'm leaving this issue under the assumption someone else might land here and find the same issue.rotu commentedon Feb 10, 2024
BTW, you can also short-circuit the circularity with an intersection, which I think is the most elegant workaround:
Also, in exploring this, I noticed that
WitnessIsParentClass<SubClass>.prototypeis typed asWitnessIsParentClass<any>. Not sure the implications or if thatanymight cause trouble with this approach down the line.typescript-bot commentedon Feb 13, 2024
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
DavidArchibald commentedon Feb 14, 2024
Dan Rose (@rotu) That would be related to my other issue, #57317.