Skip to content

Cannot infer generic argument type from passed callback #31146

Description

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms:

infer, parameter, argument, callback, function

Code

function inferArguments<T>(callback: ((t: T) => void)) {
  return callback;
}

function noop(){}

const explicit = inferArguments(({a = noop}: {a: Function}) => {});
explicit({a: noop}); // OK!
explicit({a: false}); // Expected error - Got one!

const implicit = inferArguments(({a = noop}) => {});
implicit({a: noop}); // OK!
implicit({a: false}); // Expected error - No Error!

Expected behavior:
Both function calls with ({a: false}) should cause a type error

Actual behavior:
Only the function call with the explicit typing causes a type error

Playground Link

Related Issues: #30975 Looks similar but seems different since there should be a clear way for inference to work in this case

Note that Parameters is correctly able to extract the correct types for such a construct as seen in this bit of code:

function noop() { }
function callback({ a = noop }) { }
let args: Parameters<typeof callback>;
args[0].a as Function

This appears to be an error with inline callback functions used in this fashion

Activity

  1. kolodny commented on Apr 29, 2019

    @kolodny
    Author

    Another thing I noticed is that this works for functions declared on the same scope but fails for function expressions passed in to the HOF

    const inferArguments = <T>(callback: T) => callback;
    
    function noop(){}
    
    function takesNoop({ a = noop}) {}
    const OK = inferArguments(takesNoop);
    OK({a: noop}); // OK!
    OK({b: noop}); // Expected error - Got one!
    OK({a: false}); // Expected error - Got one!
    
    const BAD = inferArguments(function takesNoop({ a = noop}) {});
    BAD({a: noop}); // OK!
    BAD({b: noop}); // Expected error - No Error!
    BAD({a: false}); // Expected error - No Error!
  2. weswigham commented on May 8, 2019

    @weswigham
    Member

    Set noImplicitAny to true and you'll see the issue is really that we issue a noImplicitAny error on implicit (though it seems like we infer all the types for inferArguments just fine, so it's odd).

  3. adrianheine commented on May 27, 2019

    @adrianheine

    I have a maybe related issue:

    function f(a) { // Would emit an error with noImplicitAny
      return () => a
    }
    const x = f(1) // I would have expected `x` to be `() => number`, but it is `() => any`
    const y: string = x() // Passes
    function f<T>(a: T) {
      return () => a
    }
    const x = f(1) // `() => number`
    const y: string = x() // Fails
  4. added
    BugA bug in TypeScript
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Aug 22, 2019
  5. RyanCavanaugh commented on Aug 22, 2019

    @RyanCavanaugh
    Member

    Looks like a bug to me - the default in the binding pattern should have made a candidate for T

  6. pradyuman commented on Nov 14, 2019

    @pradyuman

    Any update on this? I'm running into an issue that is perhaps related. In the following function, Res never gets inferred.

    export const execGrpc = <Req, Res extends {}>(
      call: (
        request: Req,
        callback: (error: ServiceError | null, response: Res) => void
      ) => ClientUnaryCall,
      req: Req
    ): Promise<Either<ServiceError, Res>>
    

    Happy to provide more details and/or help debug if it makes sense.

  7. RobertWHurst commented on Nov 16, 2019

    @RobertWHurst

    I've also run into this same issue. It would be nice to see it fixed.

  8. theonlypwner commented on Nov 19, 2019

    @theonlypwner

    A common issue is having to manually specify a type for the Promise constructor. (tested in Visual Studio Code with TypeScript 3.7.2)

    const p = new Promise((resolve) => resolve(1))

    p is of type Promise<unknown>, so in order to use it later, one has to specify the type explicitly to have p be of type Promise<number>:

    const p = new Promise<number>((resolve) => resolve(1))

    Pradyuman Vig (@pradyuman) I've also run into the same issue with making my own wrapper for gRPC calls. I found that it is due to having multiple overloads for call (the auto-generated TypeScript declarations have multiple overloads for generated methods in the client), which prevents inference from working.

    Here's a reproducible example (tested in Visual Studio Code with TypeScript 3.7.2), using number instead of gRPC request/response protobuf types:

    function f<T, U> (
      call: (
        request: U,
        innerCallback: (error: Error | null, value?: T) => void
      ) => void,
      request: U): Promise<T> {
      return new Promise((resolve, reject) => call(request, (error, response) => {
        if (error) {
          reject(error)
        } else {
          resolve(response)
        }
      }))
    }
    
    declare function p (request: number, call: (error: null, a: number) => void)
    // declare function p (request: number, a2: boolean, call: (error: null, a: number) => void)
    
    async function g () {
      const v = await f(p, 1)
      // the type of v is inferred as number
      // but if the other declaration above is uncommented, it is inferred as unknown
    }

    My workaround is to specify the type explicitly (const v: number = await f(p, 1)), which still catches type errors (like const v: boolean = await f(p, 1))

  9. Aetet commented on Nov 24, 2019

    @Aetet

    Ryan Cavanaugh (@RyanCavanaugh) this affects event react typings for typescript:
    DefinitelyTyped/DefinitelyTyped#30057 (comment)

    Because now any usage useCallback leads to silent any inference

    Any update on this?

  10. theonlypwner commented on Nov 26, 2019

    @theonlypwner

    C# can't infer the types either:

    using System;
    
    namespace ConsoleApp1
    {
        static class Program
        {
            static void f<T>(Action<Action<T>> c)
            {
                c(x => Console.WriteLine(x));
            }
    
            static void Main(string[] a)
            {
                f(x => x("test")); // error
            }
        }
    }

    Java can though:

    import java.util.function.Consumer;
    
    public class Main {
        static <T> T f(Consumer<Consumer<T>> c) {
            c.accept(x -> System.out.println(x));
            return null;
        }
    
        public static void main(String[] args) {
            String s = f(x -> x.accept("a")); // output: a
            String s2 = f(Main::g); // output: a
            // Integer i = f(x -> x.accept("a")); // does not compile, good
        }
    
        static void g(Consumer<String> c) { c.accept("a"); }
    }
  11. xpl commented on Mar 30, 2020

    @xpl

    Ran into the same issue today! Any plans to get it fixed?

  12. anilanar commented on Apr 14, 2020

    @anilanar
    Contributor

    Max Greb (@Aetet)

    Ryan Cavanaugh (@RyanCavanaugh) this affects event react typings for typescript:
    DefinitelyTyped/DefinitelyTyped#30057 (comment)

    Because now any usage useCallback leads to silent any inference

    Any update on this?

    That's because react typings don't provide typings for >TS 3.0.

    For example, we use the following useCallback in our codebase instead of using the "official" react typings:

    export const useCallback: <T extends (...args: never[]) => unknown>(
      callback: T,
      deps: Array<unknown>,
    ) => T;
    
  13. lxsmnsyc commented on Sep 21, 2020

    @lxsmnsyc

    image

    Might be related. This requires T to be defined explicitly when using test.

  14. theonlypwner commented on Sep 21, 2020

    @theonlypwner

    In TypeScript, this is the current issue (affecting new Promise):

    type Action<T> = (t: T) => void
    
    // the original example in the original post seems to be OK now, as there is an error for
    // const implicit = inferArguments(({a = noop}) => {});
    function f<T>(_: Action<T>): T { return null as any; }
    const x = f((_: string) => {}); // y has type string, OK
    
    // current issue in TypeScript 3.7 to 4.0
    function f2<T>(_: Action<Action<T>>): T { return null as any; }
    const y = f2(x => x("1")); // y has type unknown, BAD <------------------------------
    const y2 = f2((x: Action<string>) => x("1")); // y2 has type string, OK
    const y3 = f2<string>(x => x("1")); // y3 has type string, OK

    Also, it actually turns out that C# can infer the type when it is explicitly written and not a method group:

    using System;
    
    void f<T>(Action<Action<T>> c) { }
    void g(Action<string> f) { }
    
    f((Action<string> f) => f("a")); // ok
    // f(f => f("a")); // error
    // f(g); // error
    
    void f2<T>(Action<T> c) { }
    void g2(string s) { }
    
    f2((string _) => {}); // ok
    // f2(g2); // error
  15. added
    Cursed?It's likely this is extremely difficult to fix without making something else much, much worse
    on Jul 24, 2025
  16. victorafael26 commented on Sep 11, 2026

    @victorafael26

    Ran into this exact pattern in a real codebase today (React lazy-loading helper wrapping React.lazy with a pick option to support named exports). Confirms this is still present as of TypeScript 5.9.3 (also reproduced identically on the new Go-based compiler, 7.0.2) — 6+ years after this was filed and confirmed as a bug.

    Minimal repro (isolated, no React):

    declare function lazyLoad<T>(
      factory: () => Promise<any>,
      opts: { pick: (mod: any) => { default: T } }
    ): T;
    
    declare const NamedComponent: (props: { foo: string }) => any;
    
    // Extracted + explicitly annotated pick: infers T correctly
    const pickFn = (m: any): { default: typeof NamedComponent } => ({ default: m.NamedComponent });
    const A = lazyLoad(() => Promise.resolve({}), { pick: pickFn });
    const a: string = A; // correctly errors: (props) => any is not string
    
    // Inline pick, no annotation: T silently becomes unknown, no error below
    const B = lazyLoad(() => Promise.resolve({}), { pick: (m) => ({ default: m.NamedComponent }) });
    const b: string = B; // BUG: should also error, but does not

    Playground

    In our case this means React.lazy-based components loaded via a named-export pick option silently lose all prop type-checking (IntrinsicAttributes errors, or worse, no errors at all where there should be). The workaround (extracting pick into a separately-typed const at every call site) works but is significant boilerplate for what's otherwise a clean inline pattern — real impact for anyone building this kind of "lazy + adapter" helper.

    Just want to add a data point that this is still very much alive and biting real-world code, in case it helps with prioritization.

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

    BugA bug in TypeScriptCursed?It's likely this is extremely difficult to fix without making something else much, much worseDomain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolution

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions