Search code examples
typescripttsx

Why is an explicit typing required even though the same type is inferred?


Given this code:

const foo = ({ bar: true });

const functionDeclaredOutsideJsx = (a: any) => foo;

const App = () => (
  <>
    <GenericElement
      // (parameter) a: string
      f={(a) => foo}
      g={(b) => {
        // Property 'bar' does not exist on type 'unknown'.
        b.bar;
      }}
    />

    <GenericElement
      f={(a: string) => foo}
      g={(b) => {
        b.bar;
      }}
    />

    <GenericElement
      f={functionDeclaredOutsideJsx}
      g={(b) => {
        b.bar;
      }}
    />
  </>
);


const GenericElement = <T,>(_: {
  f: (a: string) => T;
  g: (b: T) => void;
}) => (<></>)

The a argument is inferred to be a string in the first GenericElement, but not completly it seems as b is inferred to be unknown.

In the second GenericElement, b is correctly inferred to have a bar field. The only difference is the explicit typing of a.

In the third GenericElement, b is also correctly inferred to have a bar field. Here the function is declared outside the jsx block, but it's explicitly typed to any.

Why does the first GenericElement not work as expected, but the other two do?


Solution

  • This is a missing feature, as described in microsoft/TypeScript#50121.

    Traditionally TypeScript has been relatively unable to perform both generic type argument inference and contextual callback parameter type inference from the same function argument. The basic inference algorithm would see that a function's callback parameter needed to be inferred, and end up deferring inference for the whole object the function is part of until after the generic type argument inference took place. Then, by the time the generic type argument was inferred, it would be too late for the compiler to get the callback parameter type correct. Without something like a so-called full unification algorithm for inference, as discussed in microsoft/TypeScript#30134, there will always be some limitations like this, where the inference algorithm gives up.

    Typescript 4.7 introduced an improvement whereby inference inside object and array literals can take place step-by-step instead of all at once, so that inferences from earlier properties and elements can affect inferences in later properties and elements in the same object or array. This was implemented in microsoft/typeScript#48538. And indeed, this very improvement helps make a function call equivalent to your JSX version work as desired:

    GenericElement({
        f: (a) => foo,
        g: (b) => {
            b.bar; // okay in TS4.7+, but
            // error in TS4.6- with Object is of type 'unknown'.
        }
    });
    

    Unfortunately, this inference algorithm was not implemented for JSX function components; as described in a comment on microsoft/TypeScript#50121:

    [Lead TypeScript Architect] Anders [Hejlsberg] did some work to make context-sensitive functions in object literals behave better in inference in 4.7, but the JSX code is a separate codepath. The work here would be to "copy" that logic over into the JSX behavior.

    So it's a missing feature for now. The issue in GitHub is marked as Help Wanted, indicating that they'd accept PRs submitted by the community. That means if you really want to see this happen, you could implement it yourself and there's a reasonable chance it would make it into the language!

    Playground link to code