Skip to content

Incorrect type inference of common generic method over union type #49893

Closed as not planned

Description

@voqum

Bug Report

🔎 Search Terms

generic method union type

🕗 Version & Regression Information

  • This changed between versions 4.1.5 and 4.2.3

⏯ Playground Link

Playground link with relevant code

💻 Code

class Visitor1 implements IAVisitor<string>, IBVisitor<number>, ICVisitor<boolean> {
    visitA(): string {
        return "hello";
    }

    visitB(): number {
        return 42;
    }

    visitC(array: (A|B)[]): boolean {
        const result: (string|number)[] = array.map(item => item.accept(this));
        return true;
    }
}

🙁 Actual behavior

In Visitor1#visitC, in the call to accept there is an error on this: Argument of type 'this' is not assignable to parameter of type 'IAVisitor<string> & IBVisitor<string>'.

The Typescript compiler seems to arbitrarily choose the first generic type of the first interface implemented by Visitor1 to infer a type T on a single aggregated function type for item.accept. This is simply incorrect.

In TS version 4.1.5, the Typscript compiler refused to aggregate the function types at all: This expression is not callable. Each member of the union type '(<T>(visitor: IAVisitor<T>) => T) | (<T>(visitor: IBVisitor<T>) => T)' has signatures, but none of those signatures are compatible with each other.

So it seems that in version 4.2.3 some kind of incorrect type logic was applied to aggregate the union of function types.

🙂 Expected behavior

The JavaScript semantics of this call will use the accept method of the respective instance, returning string or number respectively, and the call to map will return type (string | number)[]. So, the type inferred on accept should be an overloaded function or a union of function types, whose members are generated by applying type inference on the argument this in a distributive manner:

  • Before type inference:
item.accept: (<T>(visitor: IAVisitor<T>) => T) | (<T>(visitor: IBVisitor<T>) => T)

where keeping in mind that generic T is independently free in each union member.

  • The type of this should be treated as an invariant in the type inference, e.g.:
this: IAVisitor<string> & IBVisitor<number> & ICVisitor<boolean>
  • The proper way to infer the concrete type of item.accept is to distribute the type of this over each generic member of item.accept, then unify the parameter type with the argument type, over the generic parameters. After type inference:
item.accept: (<T extends string>(visitor: IAVisitor<string>) => string) | (<T extends number>(visitor: IBVisitor<number>) => number)
  • The type of this obviously distributes over the union type of item.accept to produce the return type string | number. This matches the JavaScript semantics and the actual return type.

  • The code in Visitor2 shows that if we explicitly narrow the type of item before calling accept, the aggregated return type is as expected. Having to explicitly narrow the types this way defeats the purpose of using the visitor pattern.

Simplifying the union of function types into a single function type is problematic when generics are involed:

I guess if you wanted to simplify the union type of item.accept, a naive attempt would look like

item.accept: <T extends string | number>(visitor: IAVisitor<string> & IBVisitor<number>) => string | number

but this might be problematic if the generic functions accept had different numbers of generic arguments. Technically each generic parameter in the union needs to become a separate generic parameter on the aggregated function type and unified independently:

item.accept: <T0 extends string, T1 extends number>(visitor: IAVisitor<string> & IBVisitor<number>) => string | number

so it may be better to avoid this type of simplification.

Activity

  1. changed the title [-]Incorrect inference of type of common generic method over union type[/-] [+]Incorrect type inference of common generic method over union type[/+] on Jul 13, 2022
  2. RyanCavanaugh commented on Jul 14, 2022

    @RyanCavanaugh
    Member

    The proposed mechanism would be potentially explosive in terms of performance -- there are often unions like this where there are hundreds of different constituents with a common method whose signature can be correctly unified into a single call, and we do do that.

    to infer a type T on a single aggregated function type for item.accept. This is simply incorrect.

    It isn't incorrect, rather it rejects some programs that otherwise could be accepted, but no typechecker is capable of accepting every correct program.

  3. voqum commented on Jul 15, 2022

    @voqum
    Author

    So I can write a program to explicitly switch over each type in the union and that doesn’t explode. Yet the compiler, which can easily enumerate the same types in the union, will somehow explode? Not a compelling explanation.

    Also, the types it infers in this case are definitely incorrect. Just because it produces an incorrect result due to computational limits didn’t make it correct. There’s a difference between yielding with no answer and yielding with an incorrect answer.

  4. RyanCavanaugh commented on Jul 18, 2022

    @RyanCavanaugh
    Member

    So I can write a program to explicitly switch over each type in the union and that doesn’t explode

    Sure, but you would know if you were writing 429,981,696 lines of code, whereas someone writing a single call and getting that many different typechecks would be very surprised.

    There’s a difference between yielding with no answer and yielding with an incorrect answer.

    Again, this is not an incorrect answer. All type checkers reject some valid programs.

  5. github-actions commented on Jun 8, 2023

    @github-actions

    This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions