Open
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
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
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 thatNoInfer
can somehow break the dependency of the function return type on its parameter type, but that's not whatNoInfer
does. 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 haveNoInfer
there.As for "I'm not hoping anything" I was trying to say that you have an expectation about
NoInfer
that is not warranted. In the type(t: T, _: NoInfer<{ x: number; y: NoInfer<R> }>) => R
the type of_.other
depends onR
and 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 whatother
is, 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
NoInfer
does, 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 @jcalz has mentioned it doesn't really have anything with
NoInfer
.Your
_2
variant 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 inferR
it can do so because it has not been resolved and "fixed" (aka set in stone) yet.In the
_1
it 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 tounknown
by 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
T
and the return typeR
, so thatR
doesn'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 asNoInfer
so thatR
doesn't have to depend on it. Isn't that the purpose ofNoInfer
?On the other hand, I understand that in the workaround
NoInferR
is 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 becauseNoInfer
is 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
NoInfer
itself 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 toNoInfer
being somewhere deeper in the parameter's type. This doesn't work either after all:The
NoInfer
here is just a red herring. The core of the issue lies in how assigning contextual parameter types has to fixR
here 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
NoInfer
should prevent it from "fixing"R
because it should mean something like "deciding onR
is not up to you". But I also understand that then the compiler would have to somehow go back after discoveringR
in the return type.I tried to see if the limitation can be circumvented with an overload that doesn't have
R
in the second parameter but I've had no luck there when the property withR
is 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
NoInferR
only 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_1
outside whenR
is disregarded in the callback parameters.R
so that it typechecks correctly if the callback parameter is indeed annotated.In my opinion, the
unknown
overload shouldn't be necessary if the property withR
is disregarded in the callback signature. If anything,NoInfer
would 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
R
before it has a chance to see the return position and be inferred by it?Perhaps using
NoInfer
to 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
R
though. 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 beunknown
if 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
FooConstructor
interface withnew
signatures, 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
R
that 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 | undefined
as 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.
unknown
if argument hasNoInfer<Generic>
but returns the Generic #60922<T>(f: (get: () => T) => T) => T
#60929