Skip to content

Generic inference to unknown after using function arguments #43371

Closed

Description

@misha-erm

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

Activity

  1. misha-erm commented on Mar 25, 2021

    @misha-erm
    Author
    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

  2. ilogico commented on Mar 25, 2021

    @ilogico

    This issue might be related: 38264.

  3. RyanCavanaugh commented on Mar 25, 2021

    @RyanCavanaugh
    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 }) =>.

  4. misha-erm commented on Mar 25, 2021

    @misha-erm
    Author

    wow, thanks for the detailed answer and the workaround

  5. jkillian commented on Jul 16, 2021

    @jkillian

    Thanks for the great explanation Ryan Cavanaugh (@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,
    });
  6. RyanCavanaugh commented on Jul 16, 2021

    @RyanCavanaugh
    Member

    Jason Killian (@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?

  7. jkillian commented on Jul 16, 2021

    @jkillian

    Ryan Cavanaugh (@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 😆).

  8. tannerlinsley commented on Dec 1, 2021

    @tannerlinsley

    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 Dominik Dorfmeister 🔮 (@TkDodo)

  9. jkillian commented on Apr 25, 2022

    @jkillian

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

  10. Andarist commented on Sep 22, 2022

    @Andarist
    Contributor

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

    The issue can be closed.

  11. layerok commented on Sep 26, 2024

    @layerok

    So the order of properties is important when inferring

    For example this works

    function createModule<RequestData, FormValues>(o: {
      values: FormValues;
      mapValues: (values: FormValues) => RequestData;
      onCreate: (data: RequestData) => void;
    }) {}
    
    createModule({
      values: {
         name: 2
      },
      mapValues: (values) => ({
        title: values.name
      }),
       onCreate: (data) => data.title.toFixed(), 
    });

    But this doesn't

    function createModule<RequestData, FormValues>(o: {
      values: FormValues;
      mapValues: (values: FormValues) => RequestData;
      onCreate: (data: RequestData) => void;
    }) {}
    
    createModule({
      onCreate: (data) => data.title.toFixed(), 
      values: {
         name: 2
      },
      mapValues: (values) => ({
        title: values.name
      }),
    });
  12. Andarist commented on Sep 26, 2024

    @Andarist
    Contributor

    Yes, this is an existing limitation. Note that it isn't about properties but certainly, it's easier to run into this with object properties. Let's take a look at this example with positional arguments (TS playground):

    declare function createModule1<RequestData, FormValues>(
      values: FormValues,
      mapValues: (values: FormValues) => RequestData,
      onCreate: (data: RequestData) => void,
    ): void;
    
    // this works
    createModule1(
      {
        name: 2,
      },
      (values) => ({
        title: values.name,
      }),
      (data) => data.title.toFixed(),
    );
    
    declare function createModule2<RequestData, FormValues>(
      values: FormValues,
      onCreate: (data: RequestData) => void,
      mapValues: (values: FormValues) => RequestData,
    ): void;
    
    // this doesn't
    createModule2(
      {
        name: 2,
      },
      (data) => data.title.toFixed(), // 'data' is of type 'unknown'.(18046)
      (values) => ({
        title: values.name,
      }),
    );

    As we can see, when context-sensitive functions are involved it always has been important to have the "producer" before the "consumer".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions