Repository navigation
Open
Labels
Domain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolutionRelated to type inference performed during signature resolution or `infer` type resolutionHelp WantedYou can do thisYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some cases
Milestone
Description
🔎 Search Terms
NoInfer parameter callback destructuring
🕗 Version & Regression Information
Latest to date.
⏯ Playground Link
💻 Code
class Foo<T, R> {
constructor(readonly cb: (t: T, _: NoInfer<{ x: number; other: NoInfer<R> }>) => R) {}
}
const _1 = new Foo((name: string, { x }) => ({ name, x })); // Foo<string, unknown>
// should be Foo<string, { name: string; x: number }>
const _2 = new Foo((name: string) => ({ name })); // works: Foo<string, { name: string }>🙁 Actual behavior
infers unknown.
🙂 Expected behavior
Should ignore the parameter tagged with NoInfer and infer correctly.
Additional information about the issue
Is this intended behavior? feels like a bug to me.
If the callback parameter is destructured partially it infers unknown, but it works if the parameter is omitted.
I tried NoInfer at various locations.
Metadata
Metadata
Assignees
Labels
Domain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolutionRelated to type inference performed during signature resolution or `infer` type resolutionHelp WantedYou can do thisYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some cases
Activity
jcalz commentedon Nov 20, 2024
Note: not a TS team member
That parameter is not annotated so it can't infer from there anyway, with or without
NoInfer. I think you're hoping thatNoInfercan somehow break the dependency of the function return type on its parameter type, but that's not whatNoInferdoes. This looks more like #47599 and possibly even just a plain old design limitation.miguel-leon commentedon Nov 20, 2024
I don't know what you mean here by "not annotated" and I'm not hoping anything about
NoInfer, let alone "break the dependency" which I also don't know what you mean.If this looks ok to you, 👍.
jcalz commentedon Nov 20, 2024
{ x }is not annotated with a type. Therefore you are expecting it to be contextually typed. Since there's no type there, TypeScript cannot possibly infer the type from that position, whether or not you haveNoInferthere.As for "I'm not hoping anything" I was trying to say that you have an expectation about
NoInferthat is not warranted. In the type(t: T, _: NoInfer<{ x: number; y: NoInfer<R> }>) => Rthe type of_.otherdepends onRand so does the return type. When you pass in a function like(t: string, other) => ⋯, TS wants to know the type of the return type before it knows whatotheris, but since, syntactically, the return type might well depend on the type ofy.other, it can't do that and it fails. Your expectation here is that TS could possibly inspect the body of the function (the⋯) and see that the return type actually does not depend on the type ofy.other. But TS doesn't do this. See #47599 and the issues linked to that one.miguel-leon commentedon Nov 20, 2024
In this example
{ x }is "not annotated" like you say but it's still inferred. So I still don't know what you mean.As for you insisting in my expectations, like I say, I don't have any. Here is explained what
NoInferdoes, and I'm using it for the explained purpose.If you're trying to say this is some sort of an undocumented gotcha (for other reasons or design limitations), then ok, just say it can't be done. No need to discuss my expectations.
miguel-leon commentedon Nov 20, 2024
In any case, not wanting to continue the discussion above and getting back to the issue at topic.
I managed to do a workaround that works. playground
but it breaks again as soon as I try to merge the class with an interface. It doesn't make sense. playground
jcalz commentedon Nov 20, 2024
I… the “expected behavior” is all I was trying to say (as per the template for the issue) I sincerely don’t mean to offend, nor do I want to seem to derail your issue. I will disengage now. Good luck.
Andarist commentedon Nov 21, 2024
The behavior is a design limitation. And like Joe Calzaretta (@jcalz) has mentioned it doesn't really have anything with
NoInfer.Your
_2variant isn't context-sensitive and this its parameter types don't have to be "resolved"). So by the time compiler gets to the return expression~ from which it can inferRit can do so because it has not been resolved and "fixed" (aka set in stone) yet.In the
_1it has to resolve them before checking the function's body (and thus the return expression~). This involves fixing all the encountered type parameters with whatever the current information about it it has. This process can't be reversed and the decision can't be changed as other types could start depending on it/be derived from it. So by the time it gets to the return expression~ it's already too late. TheR's type got fixed tounknownby this time.The type there might be what you expect here but I don't think it actually works like you might expect it too: TS playground
miguel-leon commentedon Nov 21, 2024
In this example: playground
I'm annotating both
Tand the return typeR, so thatRdoesn't have to depend on anything, and it's still resolving tounknown. Nothing about the second parameter (context-sensitive or not), should matter because I'm trying to mark it asNoInferso thatRdoesn't have to depend on it. Isn't that the purpose ofNoInfer?On the other hand, I understand that in the workaround
NoInferRis meaningless inside the callback, so it doesn't really solves the problem.But what's up with workaround 2? Why does it break with a change that changes nothing?
Or is this a different discussion? I feel like there are bugs everywhere.
Welp, focusing on the original example, should the title of the issue be something like "There's no way to mark parameters in callback types as
NoInfer"? is it becauseNoInferis recognized only at the top level of the parameter type? I found this #59668Andarist commentedon Nov 21, 2024
This is an interesting case. It seems like the compiler doesn't infer from a return type annotation. It should be fairly straightforward to add this capability, I'll take a stab at it later.
As it stands, this is a design limitation and not a bug. I understand it's frustrating to hit limitations but it's not like they exist for no reason. The type checker is a complex piece of software and its design comes with some tradeoffs here or there.
No. What you report here is not about
NoInferitself but about the fact that u'd like to use the inferred type automatically in the parameter type but at the same time you'd like to infer it from the return position. This is specific to the return thing here and not toNoInferbeing somewhere deeper in the parameter's type. This doesn't work either after all:The
NoInferhere is just a red herring. The core of the issue lies in how assigning contextual parameter types has to fixRhere before it has a chance to even see the return expression~miguel-leon commentedon Nov 21, 2024
I think I understand now with this wording. Although, I still think that
NoInfershould prevent it from "fixing"Rbecause it should mean something like "deciding onRis not up to you". But I also understand that then the compiler would have to somehow go back after discoveringRin the return type.I tried to see if the limitation can be circumvented with an overload that doesn't have
Rin the second parameter but I've had no luck there when the property withRis indeed present.On workaround 2, I found out that if you extract the type to a auxiliary interface, it works again. playground. It really is shocking seeing how changes that don't change anything make it or break it.
Andarist commentedon Nov 21, 2024
It really doesn't work here. It's the same thing I mentioned at the end of this comment. You can hover over things to see that your
NoInferRonly benefits from its default type param being resolved but it still can't be used at all: TS playgroundmiguel-leon commentedon Nov 21, 2024
I meant on resolving the merge with the interface breaking it. Because you said you would have a stab at it.
I understand that the workarounds don't really help inside the callback itself. Although that doesn't matter too much, as long as the variable outside (
_1) is inferred correctly. Which is the original issue. Inside the callback, sure, you have to go ahead and annotate it.miguel-leon commentedon Nov 21, 2024
From what was discussed, I think I discerned the best hack for it to work as intended. playground
unknown(same as a generic parameter) so that it infers_1outside whenRis disregarded in the callback parameters.Rso that it typechecks correctly if the callback parameter is indeed annotated.In my opinion, the
unknownoverload shouldn't be necessary if the property withRis disregarded in the callback signature. If anything,NoInferwould be correctly interpreted by the user as "don't try to figure outR".And the alias shouldn't be necessary either, though you already mentioned it is a pretty straightforward fix.
What's concerning is that there's not a clear strategy on how to reach these "hacks", or for that matter, on how to report the usability issue here (regardless of what the compiler does behind the scene), besides throwing stuff at the wall to see what sticks.
miguel-leon commentedon Nov 22, 2024
Is it too complex to not fix
Rbefore it has a chance to see the return position and be inferred by it?Perhaps using
NoInferto tell the checker to do such thing? So that the code here works #60544 (comment)If this were to be a suggestion, is it considerable or is it too outlandish and out of the question?
Andarist commentedon Nov 22, 2024
It has to figure out the parameter's type that contains
Rthough. It might not be apparent here because you have destructured that parameter but the type for the overall parameter still has to be resolved. It would be a big undertaking to change this now. Potentially not impossible for certain scenario but the ROI seems to be pretty low. And even with such an improvement, I still wouldn't expect this to work for cases in which you actually end up destructuringother. Having part of your parameter type dependent on the return type... isn't exactly a popular pattern. To figure out it's type the compiler has to typecheck the return expression, that in turn might depend on the information about the function's parameters - it's very easy to end up with circularity issues in those scenarios.miguel-leon commentedon Nov 23, 2024
The overloads don't work on yet another edge case when you explicitly provide the types parameters in the instantiation of the class, as in
new Foo<{ name: string }, string>(...)the callback parameter would persist to beunknownif not annotated.But finally! Something that works in all use cases as you'd expect, is adding another type parameter to the class: playground
The bad thing about it is the extra useless type parameter. It could be circumvented easily if you could declare a constructor with more type parameters than the class itself, but typescript missed an easy syntax for that. You have to jump through hoops with a
FooConstructorinterface withnewsignatures, and then re-export the class to get rid of extra types parameters. That right there is another suggestion issue that would reduce a lot of code.The problematic thing in this issue is the "fixing" of
Rthat can't be prevented with anything, not evenNoInfer(which, I dare say, should, at least because of semantics). I guess it's not so difficult to achieve if programmed as a emmulation of an extra type parameter. Maybe I'll do it sometime if I finish following the 50k lines in a single file that ischecker.ts.Sorry to bother with all my reportings, it's just... it never feels right having to take so many turns and long ways to achieve something so sensible. Too few patterns are "popular", like you say, but as one example, something like a mapper visitor pattern, would easily receive a parameter
R | undefinedas the previous node and returnR, so less popular patterns should be more widely embraced.Welp, that's that, in the meantime, at least there's the minor side bug of when you merge with the interface, if it's something worth reporting.
Thank you for the back-and-forth.