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 different between equivalent function expression and arrow function expression in object literal #38845

Closed
fsenart opened this issue May 29, 2020 · 4 comments · Fixed by #48538
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Fix Available A PR has been opened for this issue

Comments

@fsenart
Copy link

fsenart commented May 29, 2020

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

@Nathan-Fenner
Copy link
Contributor

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

@fsenart
Copy link
Author

fsenart commented May 29, 2020

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
});

@RyanCavanaugh
Copy link
Member

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

@ahejlsberg
Copy link
Member

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.

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 Fix Available A PR has been opened for this issue
Projects
None yet
5 participants