microsoft/TypeScript

Generic inference different between equivalent function expression and arrow function expression in object literal

fsenart opened this issue ยท 4 comments

TypeScript Version: Nightly

Search Terms: parameter, inference, generic, function expression, arrow function expression

Expected behavior:

In function b, parameter a should be inferred as a: () => 42.

Actual behavior:

When using function expression instead of arrow function expression, parameter a is inferred as a: unknown.

Related Issues: #32230

Code

export declare function foo<A>(
    options: {
        a: A
        b: (a: A) => void;
    }
): void;

foo(
    {
        a: () => { return 42 },
        b(a) {}, // a: () => 42
    }    
);

foo(
    {
        a: function () { return 42 },
        b(a) {}, // a: unknown
    }    
);

foo(
    {
        a() { return 42 },
        b(a) {}, // a: unknown
    }    
);
Output
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
foo({
    a: () => { return 42; },
    b(a) { },
});
foo({
    a: function () { return 42; },
    b(a) { },
});
foo({
    a() { return 42; },
    b(a) { },
});
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

Just to add another data-point to the strangeness, if b is an arrow function or an anonymous function expression instead of a method-shorthand, it also gets the () => 42 type inferred for its argument:

foo(
    {
        a: () => 42,
        b: (a) => {}, // a: () => 42
    }
)
foo(
    {
        a: () => 42,
        b: function(a) {}, // a: () => 42
    }
)

so this suggests that the inference problem also has to do with how b is defined, not just a

While experimenting around this problem, I found out another strange behavior that I suspect to be related to this one:

export declare function foo<C, A>(options: {
    c: C;
    a: (c: C) => A;
    b: (b: A) => void;
}): void;

// a(): no parameter -> return type can be inferred
foo({
    c: 42,
    a: () => {}, // a: (c: number) => void
    b(b) {}, // b: (b: void) => void
});

// a(): a parameter (with inferred type) -> return type cannot be inferred
foo({
    c: 42,
    a: (c) => {}, // a: (c: number) => unknown
    b(b) {}, // b: (b: unknown) => void
});

// a(): a parameter (with explicit type) -> return type can be inferred
foo({
    c: 42,
    a: (c:number) => {}, // a: (c: number) => void
    b(b) {}, // b: (b: void) => void
});

However, if function a() is the "last link in the chain", then everything works as expected:

export declare function foo<C, A>(options: {
    c: C;
    a: (c: C) => A;
}): void;

// a(): no parameter -> return type can be inferred
foo({
    c: 42,
    a: () => {}, // a: (c: number) => void
});

// a(): a parameter -> return type can be inferred
foo({
    c: 42,
    a: (c) => {}, // a: (c: number) => void
});

@ahejlsberg I tried to reason about the context-sensitiveness of this and failed. Can you weigh in, or is this just a bug?

This is a design limitation. Similar to #38872. A arrow function with no parameters is not context sensitive, but a function expression with no parameters is context sensitive because of the implicit this parameter. Anything that is context sensitive is excluded from the first phase of type inference, which is the phase that determines the types we'll use for contextually typed parameters. So, in the original example, when the value for the a property is an arrow function, we succeed in making an inference for A before we assign a contextual type to the a parameter of b. But when the value is a function expression, we make no inferences and the a parameter is given type unknown.