Skip to content
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

NoInfer does not work in function parameters #60071

Closed
gund opened this issue Sep 26, 2024 · 10 comments
Closed

NoInfer does not work in function parameters #60071

gund opened this issue Sep 26, 2024 · 10 comments
Labels
Duplicate An existing issue was already created

Comments

@gund
Copy link

gund commented Sep 26, 2024

πŸ”Ž Search Terms

NoInfer does not work in function parameters

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.2#code/CYUwxgNghgTiAEAzArgOzAFwJYHtVNQB4AVAPgAowAjALnnOCgygH46A5HASVURBhKkAlPAC8peMSF1iAbgBQ8xKnIMmUEePgByAIwAmAMzahCgPRn4XeAHMQGOinTY8BQmgDWqHAHdUFajo1ZjZ4T28-TQlw31RpMNQvWPkLK3gfKFQHJDRMXHxlQgBnDBgsVBsA2npGELoSsoqo+Abym3jWivkgA

πŸ’» Code

In some cases I need to infer generic only from the function return but still use it to type the same function parameter:

declare function fn<T>(cb: (data?: NoInfer<T>) => T): T;

fn((data) => '123');
// I get: function fn<unknown>(cb: (data?: unknown) => unknown): unknown
// I want: function fn<string>(cb: (data?: string) => string): string

πŸ™ Actual behavior

I get the type: function fn<unknown>(cb: (data?: unknown) => unknown): unknown

πŸ™‚ Expected behavior

I expect the type: function fn<string>(cb: (data?: string) => string): string

Additional information about the issue

No response

@MartinJohns
Copy link
Contributor

Sounds like a duplicate of #56311.

@gund
Copy link
Author

gund commented Sep 26, 2024

Sounds like a duplicate of #56311.

It might be related but that issue is old and does not include NoInfer feature which was released in v5.4, way after that bug was created.

@jcalz
Copy link
Contributor

jcalz commented Sep 26, 2024

But NoInfer just blocks inference at a location and T is already not inferrable from the unannotated callback param. I’d say you’re mistakenly attributing the problem to NoInfer. This feels more like #47599 to me.

@gund
Copy link
Author

gund commented Sep 26, 2024

@jcalz I do not understand what do you mean by:

NoInfer just blocks inference at a location and T is already not inferrable from the unannotated callback param

Clearly generic T inference is not blocked by the function parameter as it's falling back to unknown for no other reason but being an inference point for that generic and the only job of NoInfer is to make sure that it's not happening.
Just to further showcase this, if you drop that function parameter - generic will be inferred as expected from the return type of the function:

declare function fn<T>(cb: () => T): T;
fn(() => '123');
// We are getting: function fn<string>(cb: () => string): string

Here is this example in a playground.

Am I missing something here?
Because to me is seems that NoInfer should exactly block inference in the function parameters, just like it does in other scenarios, but it simply does not do anything, which seems to me that this specific use-case was not implemented.

@jcalz
Copy link
Contributor

jcalz commented Sep 26, 2024

You are expecting data => '123' to be inferred as type (data?: string) => string which is a matter of contextual typing of the data parameter. But TS is unable to infer that because, in general, it needs to know the types of a function's parameters to know its return type. (e.g., what's the return type of data => data??"123"?) In a case like data => '123' this is easy because the return type does not depend on anything. But then you could have written () => '123' instead. And if you do that, your inference will suddenly work with or without NoInfer:

declare function fn<T>(cb: (arg: T) => T): T;
fn(() => '123'); // string

declare function fn2<T>(cb: (data?: NoInfer<T>) => T): T;
fn2(() => '123'); // string

TypeScript looks at data => '123' and defers typing it until it knows what data is, but by then it is too late for inference to happen the way you want. The inference fails and T falls back to unknown. Falling back to unknown is just a failure of inference. It does not mean that TS looks at data and infers unknown from it, so putting NoInfer<T> there doesn't prevent the problem.

It looks like you ran into the problem of context-sensitive generic interactions, such as that discussed in #47599, tried to use NoInfer to "block" inference from a place it wasn't happening anyway, saw that this failed, and interpreted this as a failure of NoInfer to act as advertised. But that's just not the case. If there's ever any solution to your problem, it will probably not involve NoInfer.

And there might never be a solution. I think they've declined previous suggestions to have TS determine whether the return type of a function implementation is independent of any of its unannotated parameter types, which is what you'd need here. The easy cases where someone could simply drop a parameter can be done by the user (e.g., write ()=>'123' instead of data => '123'), and then you have all kinds of more complex cases where TS would have to do lots of extra work.

@RyanCavanaugh
Copy link
Member

Agree with @jcalz on this. Fundamentally there are two cases here:

  • Your function return type depends on the type of data. In this case, you have a circular dependency and nothing particularly meaningful can be resolved
  • Your function return type doesn't depend on the type of data. In this case, you can remove it from the parameter list

Despite appearances, there isn't a third case where data is read but doesn't have a possible effect on the return type. If data is read, then it has impacts on control flow analysis, and CFA can effect which return statements are reachable and which aren't (thus changing the return type).

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Sep 26, 2024
@gund
Copy link
Author

gund commented Sep 26, 2024

Thanks @jcalz for detailed explanation it actually now makes sense why it's not related to the NoInfer. I think I had a different expectation of what it was doing and it seemed like it was not doing it's job, but it's not even involved in this use-case.

My use-case is like the first one that @RyanCavanaugh mentioned - I have a function that accepts previous data and creates a new one based on it, something like a reducer, with some minor props access inside. So it's kind of recursive but it makes sense to type the function based on the return type when the type is defined in the return type of the function it calls internally:

function makeReducer<T>(fn: (prev?: T) => T): T;

// Here I do not really want to type function parameter but rather infer it from used inner function's return type
makeReducer(prev => processState(prev.state));

// The function typing is defined here
function processState(state?: State): FullState;

It seems that the underlying issues is that the inner function here has different input and output types while the function parameter expects both input and output types to be the same - so we cannot simply pass fully typed function as a parameter but need to define intermediate function which translates from out input type to another, but it currently requires us to specify full types of that intermediate function even though it's in theory possible to infer it's signature as we have all the necessary information.

I'm wondering if this use-case would be considered in the #47599 which you mentioned above or should I maybe create another issue to track it?

@jcalz
Copy link
Contributor

jcalz commented Sep 26, 2024

Maybe not #47599; Looks more like #56311 as @MartinJohns mentioned (and thus #49618). I don't know if your case is distinct enough from those to open a new issue, but it looks like everyone in those threads are saying it's probably too complex for the case where it actually matters, like foo => f(foo). Lots of extra work for the TS compiler, more potential control flow analysis circularity to trip over, all to avoid annotating your function parameters. It might be nice for x => document.getElementById(x) to infer that x is string, but that's just backwards from how inference currently works in TS, and the workaround is to just write (x: string) => yourself. Anyway I'm not a TS team member so I don't speak authoritatively. Good luck!

@gund
Copy link
Author

gund commented Sep 26, 2024

Thanks for clarification, I will close this issue.

@gund gund closed this as completed Sep 26, 2024
@RyanCavanaugh
Copy link
Member

The provided example

// Here I do not really want to type function parameter but rather infer it from used inner function's return type
makeReducer(prev => processState(prev.state));

is probably a "further down the rabbit hole" bullet item on #47599 where one of the deferral cases is functions whose return expressions are all calls to un-overloaded non-generic functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants