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

Generic inference to unknown after using function arguments #43371

Closed
misha-erm opened this issue Mar 25, 2021 · 10 comments
Closed

Generic inference to unknown after using function arguments #43371

misha-erm opened this issue Mar 25, 2021 · 10 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@misha-erm
Copy link

Bug Report

Hello 👋🏻
I'm facing some strange issue with Generics. Until I specify input arguments in function all generics are inferred correctly. But as soon as I mention argument in function the inference is broken.

Thanks in advance for any help 🙏🏻

🔎 Search Terms

  • generic is lost after specifying input arguments
  • typescript generic unknown after specifying arguments (found this stackoverflow but not sure if it's the same case)

🕗 Version & Regression Information

typescript: 4.2.3
Also tried nightly build but the issue remains

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

⏯ Playground Link

Workbench Repro

💻 Code

type Connector = <
    AccessToken
>(opts: {
    getAccessToken(input: {code: string}): Promise<AccessToken>;
    validateAuth(input: {fields: AccessToken}): Promise<{name: string}>;
}) => any;

const connector: Connector = (inp) => undefined

connector({
    getAccessToken: async () => ({token: 'token'}),
    validateAuth: async ({fields}) => { //fields: {token: string}
        //                  ^?
        return {name: 'qwe'}
    }
})

connector({
    getAccessToken: async (inp) => ({token: 'token'}), // mention input argument breaks inference
    validateAuth: async ({fields}) => { // fields: unknown
        //                  ^?
        return {name: 'qwe'}
    }
})

🙁 Actual behavior

fields inside validateAuth infer to unknown after using function argument in getAccessToken. Until I use arguments everything works OK

🙂 Expected behavior

Usage of function arguments inside getAccessToken should not affect Generic inference since they are statically type

@misha-erm
Copy link
Author

misha-erm commented Mar 25, 2021

type Connector = <
    AccessToken
>(optsA: {
    getAccessToken(input: {code: string}): Promise<AccessToken>;
}, optsB: {
    validateAuth(input: {fields: AccessToken}): Promise<{name: string}>;
}) => any;

const connector: Connector = (inp) => undefined

connector({
    getAccessToken: async () => ({token: 'token'}),
    
}, {
validateAuth: async ({fields}) => { //fields: {token: string}
        //                  ^?
        return {name: 'qwe'}
    }
})

connector({
    getAccessToken: async (inp) => ({token: 'token'}), // mention input argument breaks inference
    
}, {
    validateAuth: async ({fields}) => { // fields: {token: string}
        //                  ^?
        return {name: 'qwe'}
    }
})

Workbench Repro

@ilogico
Copy link

ilogico commented Mar 25, 2021

This issue might be related: 38264.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Mar 25, 2021
@RyanCavanaugh
Copy link
Member

Our inference algorithm is based on doing a fixed number of passes to collect inference candidates (see also #30134 which proposes that we use an unbounded number of passes). In this case, the parameterless function is non-context-sensitive, meaning that its return type can be known ahead-of-time to not depend on the ultimate result of inference, so we're able to use its return type to collect an inference candidate.

The parameterful version is context-sensitive, because it has an unannotated parameter. From the outside we can see that this ultimately won't matter, but the inference process is not far enough along at this point to use that information, and thus delays collecting the return type as an inference candidate until later on at which point AccessCode has already been fixed to unknown due to a lack of covariant candidates.

The workaround in this case is to write (inp: { code: string }) =>.

@misha-erm
Copy link
Author

wow, thanks for the detailed answer and the workaround

@jkillian
Copy link

Thanks for the great explanation @RyanCavanaugh, very helpful! Ran into this exact issue today (playground link), and it was especially insidious because it silently introduced anys into the codebase without causing any compiler errors. Leaving a slightly simplified example here just in case it's useful for discussions/example use cases that could be improved. It'd be much less of a priority for me if it just introduced unknowns instead of anys, because that would be caught easily in other places at compile time:

type InputData = { someNum: number; }

type Options<T> = {
  selector: (data: InputData) => T;
  equalityFn?: (a: T, b: T) => boolean;
}

function useSelector<T>(options: Options<T>): T {
  return options.selector({someNum: 20});
}

// GOOD: works when there's an implict type for `data` and no `equalityFn`,
// `foo` is correctly of type `number`
const { foo } = useSelector({
  selector: (data) => ({ foo: data.someNum }),
});

// GOOD: works when there's an expicit type for `data` and an `equalityFn`
// `foo2` is correctly of type `number`
const { foo2 } = useSelector({
  selector: (data: InputData) => ({ foo2: data.someNum }),
  equalityFn: (a,b) => a === b,
});

// BAD: fails silently when there's an implicit type for `data` and an `equalityFn`
// foo3 is `any`
const { foo3 } = useSelector({
  selector: (data) => ({ foo3: data.someNum }),
  equalityFn: (a,b) => a === b,
});

@RyanCavanaugh
Copy link
Member

@jkillian that's a bug caused by the binding pattern (aka destructuring). I think it's reported already but can't find it at the moment - can you log a new issue?

@jkillian
Copy link

@RyanCavanaugh, thanks, agreed after looking at it again that the any vs. unknown part of my above post is a different issue than this ticket about function arguments and inference. I've made the new issue for that here: #45074. Please feel free to edit anything as necessary if I used wrong terminology anywhere (as is highly likely 😆).

@tannerlinsley
Copy link

tannerlinsley commented Dec 1, 2021

Closed #46977 (duplicate) for this one. We're trying to do get around this in our React Query library and it's definitely been weirding us out 😂 . I think it's worth dropping in a quick snippet of our use-case for posterity:

function useQuery<TQueryKey, TData>(_options: {
  queryKey: TQueryKey;
  queryFn: (context?: { queryKey: TQueryKey }) => TData;
  onSuccess: (data: TData) => void;
}) {}

const queryKey = ["test", 1, 2, { 3: true }]

// no context no cry
useQuery({
  queryKey,
  queryFn: (): number => 1,
  onSuccess: (data) => data.toFixed(),
});

// as soon as I use ctx, data is no longer of type number for onSuccess
useQuery({
  queryKey,
  queryFn: (ctx): number => 1,
  // Why is `data` of type `any`?
  onSuccess: (data) => data.toFixed(),
});

For now, we've dropped the proposal to adopt this syntax as the primary syntax in our API, but we would really love to make it happen in the future. Where should we go from here to help that happen? cc @TkDodo

@jkillian
Copy link

I believe this is related to #47599 and is now fixed by #48538?

@Andarist
Copy link
Contributor

@RyanCavanaugh it seems that this got fixed by intra-inference improvements in 4.7, all reported playgrounds work:

The issue can be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants