-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Recursive types can depend on presence and source location of unused type declaration #55758
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
The way to think of this isn't that removing Probably a simpler bottom three lines is: const jsonRoot = () => jsonObject;
jsonRoot(); // <- replacement for _magic
const jsonObject = map(jsonRoot); The circularity in the
This all starts at
These would certainly be nice invariants to have, but the best-case scenario here would really involve erroring more often rather than erroring less often, which isn't something anyone is really asking for in most cases (indeed, telling someone we introduced an error in their code so that someone else experienced better consistency wouldn't be a well-received strategy). TL;DR not getting a circularity error in code with a legitimate circularity (even one that could be resolved in a finite number of hypothetical levels of reasoning) is like getting a walk-in table at a restaurant -- it may happen but is not guaranteed to occur again even in facially-similar situations. |
Personally, I find introducing runtime behavior which accesses a constant before its defined more "complex" then an unused type declaration. That said, this example does provide another perspective which has helped me understand, so thanks for that.
I'm likely missing something (since I don't know the implementation, and I imagine its complex and has a lot of constraints), but this explanation suggests a possible resolution of this problem: There is no need to answer "Is that a legal call?" when computing the type of "the result of calling More explicitly if we do: delay checking if the call is valid until after computing the result of the call. Once you have the result (which might be an error), then check if the call is valid, and if thats an error, use that error (to avoid changing which error gets surfaces to users in the case that both were errors).
It seems to me like the reordering suggested above might fix a third invariant (that I didn't list) but would like to have:
If we defer evaluating of usages against the extends clauses, then their presence no longer changes the evaluation order of recursive types in a way that is sometimes causes them to not compile.
I am aware of this. My hope is this specific case seems like it might be improvable since there are so many different small tweaks that a programmer can to into tricking it to work that it seems like some tweaking in the compiler could do those automatically. If you can trick the compiler into imagining the extra type declaration that fixes things always exists even when you don't code it, or that the extends clause that breaks things doesn't exist until after the type is figured out (or is replaced by a second type check afterwards), it would just work. Edit: I guess simpler phrasing of my suggestion is solve what types could be before checking if that meets all the constraints (like extends clauses). I believe that strictly increases which types will build, but I understand that might not be practical, but at least in this case the extends check seems practical to defer. |
A different approach to mitigating the error, this time using "satisfies" in the playground. I think this one is worth highlighting since what it doing is just checking that the typeof a value satisfies the extends constraint it's about to be used with. This really seems equivalent to just reordering the checks the compiler does slightly, and seems like something the compiler should be able to do automatically. I understand if this is intractable since doing it in this order would add to much overhead or break other things, but it really seems like it should be possible to make cases like these compile in the compiler without requiring these source level tweaks. |
Another interesting find: when these types to work correctly (type check properly, and compile with out inferring "any"), the generated d.ts files include "any" instead of the recursive type reference. This happens even in the cleanest example: Should that be considered part of this issue (and a design limitation), or is the fact that the d.ts generation is diverged from the normal type checking (and infers any) a separate issue which I should fine independently? I think that is a pretty serious bug since when working with no-implicit-any, generating implicit any, with no error, and it only shows up for users of the library (which will also not get an error if using no-implicit any) is pretty likely to cause actual bugs. |
Here is a simpler example of the d.ts issue: const a = () => a; Produces the d.ts of: declare const a: () => any; It should instead produce: declare const a: () => typeof a; Given this trivial reproduction, and what seems like a clear violation of no-implicit-any without a compile error, I'll file a separate bug to track that. |
I have file 55832 to track the issue with |
## Description Adopts a new pattern for documenting workarounds for microsoft/TypeScript#55758, making the documentation more centralized, and more clear about what the desired extends clauses are. Also avoids using the constructor for FieldSchema and instead use static builders whose type parameters can be constrained. This, when combined with `const` generic type parameters removes the reason for existence of the generic FieldSchema builders on SchemaBuilder, so those have been removed (the ones which depend on specific field kinds are kept). Also adds some runtime validation in FieldSchema for cases the type system can't fully handle. ## Breaking Changes Users of SchemaBuilder.field (or new FieldSchema) and SchmeaBuilder.fieldRecursive should use FieldSchema.create and FieldSchema.createUnsafe.
I think the order dependent nature of this type checking causes intelisense in VSCode to not work correctly. It can show the recursive type as type checking properly, then if you modify the code to introduce an error, and undo your change, it claims the type is self dependent and typed as "any". This bad state persists until I reload the TypeScript project. |
The thing that makes it work when you add the intervening Without the call, pretty much everything is the same, except the info in the pushTypeResolution stack says we're trying to resolve the type It might be easy to make an overly-targeted fix for cases where the return type of some function can be trivially determined to be the type of some other symbol so the circularity stack can support some comparisons between resolving return types and resolving variable types, but I don't know how valuable that would be. The other theoretical approach I can think of is the kind of speculative checking we often wish we had but have found too challenging to roll back all affected stateβif we had the ability to attempt to get a type inside a closure that rewound all state upon a circularity error, it seems like we would just use that instead of In short, I couldnβt find a good way around this. |
@weswigham Looks to me like this issue was caused by this commit some four years ago. That commit was part of #30416, but I don't really understand how it relates to the issue that PR is fixing. The commit causes us to silently ignore circularities when we detect them during signature relations, and that in turn leads to strange effects such as the issue reported here. If I remove the commit, the repro in this issue consistently errors (as expected). It also causes a circularity error in this test added by #30416, but I don't see how that test relates to the issue fixed by the PR and I'm not sure there's really anything wrong with the error in the test. |
Adding the So I don't think that test should be disregarded - if it gets an error, some stuff in the wild likely will, too. Considering this issue, I think the reentrant |
@weswigham My preference would be to just accept the circularity error in the test from #30416, but feel free to explore other solutions. I'll assign the issue to you for now. |
π Search Terms
recursive type, unused, compile error
π Version & Regression Information
β― Playground Link
https://www.typescriptlang.org/play?#code/PQKhCgAIUh1AnAhgB2QUwCaQJYDtKKQBmArrgMYAu2A9vpTQQDZM0Duk8aRa88eAc0gNhAT3QBnSACNuNLsIAWaUQQUY05Joi4YAdFBgAVRdikBbRAGs0UruRLwJ2AG5oCuLORoBae4+c3MUlIZBoJZ2kmNANoYHBKcXcjJIAlbl40CncAXkgACgBKSByAPkgjLjQAZXJlSwAeFPR0ni5s0oBucHBQCGhIAGEac2RsaOFsc3dECN5KJUQFgEEcKVnnAVxEKPcRfLQAD0osjAligCFY40YSCTQAGkhnUaZVDSI8GeC0AC5DSAAA0S6EgAH06porCVOGgAI4kbBcZYRbBbHbRIw0JpPACih3QVEwXUBhniIPcXARSLQKM2212WIaYNWRxOnikFyeF3KeUo8BIaG64C0GwqVVq9UQTXKAG9wABIZAkKLYciQby4CT8khUeT5ZWq9VcRAYOhvHC4Ig0X4VYqygC+4CdvWAQxQlEcthkNEoih+HiwAtw1Gmlut8Es1DoxHgIyU7mQOkQ0xO8AMwDd6XMNBcggTQLZpykzTQrUy2UBkEsNikfrMGpoGkbo3Gj0gaDc+GwRALlgEasgTC+MjQrA4De85mmIcwkBoJEoBlIFGj+EsyCaHeOxYqaQy7XIaFK+QpEltRkKF4lkMaRjliq4nvg+FwaA4lTQNVviFPSXO3Qupq2qQAAVhIdCpDQvowkUJTlOBdAAPLSKBmiUMKmZDCMM7ULgQgLgs9ZSMOb7zou87wNWub5tgRGMH67jSL6DDmPOvaMcQbYaogdzesRjbNiIuAwVOYzRBmbomA2DZuPAqhNhgtpcDmeb4ZAZB8VgFKQB8eB0bQWrPIoC5MFgIkLNIJrQmJ4xLIZBg6WC-aDnkVKIsiqLooy2IUjQvaIbgUG+k8pbloex7CsBCyBShaFUDCG75IFwWUIUnRAA
π» Code
π Actual behavior
This code fails to compile if the "_magic" line (which declares an unused type) is removed, or moved to the bottom of the file.
π Expected behavior
Ideally this code should compile, regardless of if the "_magic" line is present.
Additionally:
Additional information about the issue
This was minified from https://github.com/microsoft/FluidFramework/blob/main/experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts where I discovered that the type assertion I added caused the code to build without having to use the special recursive type workaround methods I added (which avoid using "extends" clauses which seem to break the recursive case). I'd love for cases like this to compile as it would make our API much nicer (the magic type assertion like to fix the build would be needed in our user's code and may require refactoring to add, so it's not a great workaround).
The text was updated successfully, but these errors were encountered: