microsoft/TypeScript

NoInfer not working on callback parameter destructuring

miguel-leon opened this issue Β· 13 comments

πŸ”Ž Search Terms

NoInfer parameter callback destructuring

πŸ•— Version & Regression Information

Latest to date.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/MYGwhgzhAEBiD28A8AVANNASgPmgbwCgBIYeAOwgBcAnAV2EvmoApqBTMAE3JAE9pgAIwBc0ZpVHpoAfVEA5eAEkyAMzbUkeaAA9RZWgFtB6gNzR4lABbr5S1eqQ5oAX2wBKaAF5cmD3mfEAPSBAuRUdAxMrBzcZHwCImIS0FKy+Dp6hsbUZhbW1LbKahpOzh7eWH4BRMGhFDT0jCzsXDz8QqLikhhpCkUOWrrQ+kam5lY2WC7uXj5VBAEEpPUyAIxew2wA7nCIzMxkYAZsouEAlmQA5hiDLuW4zFqHxxjad25mtQjI51cYtGQANZkeBbMjYAi1CCWeC0ECcaDGXY-GgXa7pZ4naC-S5mIYjbLTAhLMKUGQAJg2ZG2yP2mNOqKu9zETyObHenxCWyYgIgom+SBxN2GbIZ1DRRKAA

πŸ’» 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.

jcalz commented

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 that NoInfer can somehow break the dependency of the function return type on its parameter type, but that's not what NoInfer does. This looks more like #47599 and possibly even just a plain old design limitation.

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 commented

{ 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 have NoInfer 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 on R 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 what other is, but since, syntactically, the return type might well depend on the type of y.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 of y.other. But TS doesn't do this. See #47599 and the issues linked to that one.

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.

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

class Foo<T, R> {
	constructor(readonly cb: <NoInferR = R>(t: T, _: { x: number; other: NoInferR }) => R) {}
}
const _1 = new Foo((name: string, { x }) => ({ name, x })); // Foo<string, { name: string; x: number }>

but it breaks again as soon as I try to merge the class with an interface. It doesn't make sense. playground

class Foo<T, R> {
	constructor(readonly cb: <NoInferR = R>(t: T, _: { x: number; other: NoInferR }) => R) {}
}

interface Foo<T, R> {}

const _1 = new Foo((name: string, { x }) => ({ name, x })); // Foo<string, unknown> again
jcalz commented

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.