Skip to content

Unrelated overload signature affects inference #25917

Description

In fixing a problem with the typings for the RxJS first and last operators, I've stumbled across some weird behaviour with overload signatures.

TypeScript Version: next (3.1.0-dev.20180725)

Search Terms:

overload signature inference unrelated incorrect

Code

interface Foo<T> {}
interface Bar<T, S> {}

export function foo<T, S extends T>(predicate: (value: T) => value is S, defaultValue?: S): Bar<T, S>;
export function foo<T>(predicate: (value: T) => boolean, defaultValue?: T): Foo<T>;
export function foo<T>(predicate: (value: T) => boolean, defaultValue?: any): Bar<T, any> | Foo<T> {
  throw new Error('Unimplemented');
}

For the behaviour to be effected, strictFunctionTypes must be true and strictNullChecks must be false:

{
  "compilerOptions": {
    "strictFunctionTypes": true,
    "strictNullChecks": false
  },
  "files": ["index.ts"]
}

Expected behavior:

When passing a function that is not a user-defined type guard, I'd expect the inferred type to be independent of whether or not the type-guard-accepting overload signature is available or is commented out.

Actual behavior:

However, if the signature that accepts a type guard is available, the inferred type - for a call that does not involve a user-defined type guard - will be:

const predicated = foo(x => x === 's', 's'); // Foo<"s">

And if the signature that accepts a type guard is commented out, the inferred type - for a call that does not involve a user-defined type guard - will be:

const predicated = foo(x => x === 's', 's'); // Foo<string>

In RxJS, this causes a problem when the operators are used with pipe. If a return type with a string literal is inferred - e.g. MonoTypeOperatorFunction<"s"> - an error will be effected if the operators are used with a string source, as string won't be assignable to "s".

Also, why is the behaviour dependent upon the above-mentioned compiler options? In particular, why does it depend upon the strictNullChecks option?

Related Issues: None found.

Activity

  1. ghost added
    BugA bug in TypeScript
    on Jul 25, 2018
  2. added
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    BugA bug in TypeScript
    on Jul 25, 2018
  3. mhegazy commented on Jul 25, 2018

    @mhegazy
    Contributor

    This is a design limitation of how overload resolution is implemented. when the compiler is trying to resolve overloads it goes through them in order one at a time trying to see which is a fit. The arguments to the function can be "contextually sensitive", i.e. their type depends on the containing context, for instance function expressions or object literals. in this case the compiler will try to infer a contextual type to go with the function expression. the type is then used to contextually type the function expression, and then check the body of the function to infer the return type. if that fits, then good, the signature is picked, if that does not the compiler moves on to the next overload. the issue here is once a function expression gets a contextual type, there is no going back. the compiler can not just remove the type and recheck it (that is not how it is implemented). so what happens is that the next overload will have the same contextual type from the previous overload, ({} in the OP). So though the first overload was not picked, it did contribute to the type of your function expression.

    A better implementation of overload resolution would be one that can erase contextual types, and try others without side effects. this, however, is a tantamount to a complete rewrite to the overload resolution logic, and has some drastic impacts on performance.

  4. cartant commented on Jul 25, 2018

    @cartant
    Author

    Andy (Andrewkraft) (@Andy-MS) The behaviour is sensitive to the compiler options mentioned in the issue:

    "strictFunctionTypes": true,
    "strictNullChecks": false

    For example, if strictNullChecks is true, the inferred type will be Foo<{}>.

  5. cartant commented on Jul 25, 2018

    @cartant
    Author

    Mohamed Hegazy (@mhegazy) Why is the behaviour different - with Foo<{}> inferred instead of Foo<"s"> - if the seemingly unrelated strictNullChecks compiler option is true instead of false?

  6. mhegazy commented on Jul 25, 2018

    @mhegazy
    Contributor

    I am not seeing that on the latest version of the compiler.

  7. cartant commented on Jul 26, 2018

    @cartant
    Author

    Apologies. I thought that VS Code used the local version of TypeScript - as that's what the linting extension does. I can see now that I was wrong. (In fact, the TS version used by VS Code is indicated in the toolbar.)

    I created a small compiler API harness and the behaviour's dependency on the compiler-options appears to have been addressed in version 3.0, as the 3.0.0-rc release infers Foo<{}> regardless of the compiler options.

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