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

Empty rest parameter type inferred from candidate function with no rest parameters when "best match" contains a contextually typed argument #52227

Open
MikeRippon opened this issue Jan 13, 2023 · 4 comments
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@MikeRippon
Copy link

MikeRippon commented Jan 13, 2023

Bug Report

πŸ”Ž Search Terms

Rest parameter inference contextual

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type inference

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

const doSomething = <Args extends unknown[]>(
    fn1: (aNumber: number, ...rest: Args) => void,
    fn2: (aNumber: number, ...rest: Args) => void,
) => {}

// OK, no contextual types
doSomething(
    (explicit: number, rest: string) => {}, // Args = [string]
    () => {},
)

// OK (both contain contextually typed arg)
doSomething(
    (contextual, rest: string) => {}, // Args = [string]
    (contextual) => {},
)

// Contextually type only fn1 - Not ok
doSomething(
    (contextual, rest: string) => {}, // ERROR
    () => {}, // Args = []
)
doSomething(
    (contextual, rest: string) => {}, // ERROR
    (explicit: number) => {}, // Args = []
)

πŸ™ Actual behavior

When fn1 (and only fn1) contains a contextually typed argument, it is ignored in favour of fn2

πŸ™‚ Expected behavior

In all of the examples, fn2 specifies no rest parameters at all.

I would expect functions that do not specify any rest parameters to always be the least preferable candidate (lowest weight?) for rest parameter inference, since "no rest parameters" can always be assigned to any rest parameter type.

@MikeRippon
Copy link
Author

MikeRippon commented Jan 13, 2023

Just in case the bug-report is a bit abstract/minimalist, here's my real-world use case to demonstrate why the behaviour feels surprising in practice. Very open to suggestions on whether there's a workaround that will hint TS to always use the asyncOperation function for inference.

Playground link

// React hook for async operations
const useAsync = <Args extends unknown[], Result>(
    asyncOperation: (
        progressListener: (progress: number) => void,
        ...args: Args
    ) => Promise<Result>,
    onSuccess?: (
        result: Result,
        ...args: Args
    ) => void,
) => { /* snip */ }

// Ok
const measureStringRequestOk = useAsync(
    async (_progressListener, stringToMeasure: string) => stringToMeasure.length,
    (length) => console.log(`String is ${length} chars long`)
)

// Ok
const measureStringRequestOk2 = useAsync(
    async (_progressListener, stringToMeasure: string) => stringToMeasure.length,
    (length, stringToMeasure) => console.log(`${stringToMeasure} is ${length} chars long`)
)

// Huh?
const measureStringRequestBroke = useAsync(
    async (_progressListener, stringToMeasure: string) => stringToMeasure.length, // Error!
    () => console.log("Measured the string!")
)

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Help Wanted You can do this Experience Enhancement Noncontroversial enhancements labels Jan 13, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jan 13, 2023
@Andarist
Copy link
Contributor

Andarist commented Feb 5, 2023

The "classic" NoInfer trick comes helpful here: TS playground

@Andarist
Copy link
Contributor

Andarist commented Feb 5, 2023

The issue here doesn't even have anything to do with contextual types, we can repro it with explicitly annotated functions (TS playground):

const doSomething = <Args extends unknown[]>(
    fn1: (aNumber: number, ...rest: Args) => void,
    fn2: (aNumber: number, ...rest: Args) => void,
) => {}

doSomething(
    (contextual: number) => {},
    (contextual: number, rest: string) => {}, // error
)

It's dependent on the order in which contravariance candidates are pushed into the array. You have experienced it with contextual type parameters because candidates from non-context sensitive functions are pushed first there.

Both candidates are available when needed but the getContravariantInference selected the left most one through getCommonSubtype.

To fix this the compiler would probably have to track "origin" of those candidates and use what you have suggested as the additional heuristic:

I would expect functions that do not specify any rest parameters to always be the least preferable candidate (lowest weight?) for rest parameter inference, since "no rest parameters" can always be assigned to any rest parameter type.

@MikeRippon
Copy link
Author

Ahh ok, that makes sense. I thought I tried re-creating it without the contextual type parameters without success, but I guess that's what happens when I try to understand compiler behaviour on a Friday afternoon!

Thanks so much for the NoInfer tip, that's going to make a huge difference πŸ‘

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants