microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.73k stars 12.45k forks source link

3.6 regression: unions of callable types involving `this` types #33379

Open rraval opened 5 years ago

rraval commented 5 years ago

TypeScript Version: Version 3.6.3, Version 3.7.0-dev.20190911

Search Terms: intersect function

Code

class A {
    public uniqueToA = "a";

    public getString(this: A): string {
        return "a";
    }
}

class B {
    public uniqueToB = "b";

    public getString(this: B): string {
        return "b";
    }
}

function f(model: A | B): string {
    return model.getString();
}

Expected behavior:

In Typescript 3.5.3, the model.getString() in function f has no error.

Actual behavior:

In Typescript 3.6.3 and 3.7.0-dev.20190911, the code above gets the following errors:

index.ts:18:12 - error TS2684: The 'this' context of type 'A | B' is not assignable to method's 'this' of type 'A & B'.
  Type 'A' is not assignable to type 'A & B'.
    Property 'uniqueToB' is missing in type 'A' but required in type 'B'.

18     return model.getString();
              ~~~~~

  index.ts:10:12
    10     public uniqueToB = "b";
                  ~~~~~~~~~
    'uniqueToB' is declared here.

Playground Link: http://www.typescriptlang.org/play/#code/MYGwhgzhAECC0G8BQ1XQA4FcBGICWw0mAdngI6YCmAKgPbwC80ARGMwNxIppa4HQBzSgBcAysIBOeYgIAUwgBZ4IALjgBKNREnSBibmjQSRmCcRZtOhgL5JbSUJBgAhfYd75CJclTqumzNgcXO44noIi4lIy8kqq0M6a0NrResiGhsbCpuaBwTZ2XABmJMDCeLTmRbIAtrQAJpQgavAAPglJKbpuRiZm0HWNIAB0QmI6Meqc1kA

Related Issues: #32506

AnyhowStep commented 5 years ago

I can see how this is super inconvenient.

I'd like to just put this silly example here, though,

class A {
    public uniqueToA = "a";

    public getString(this: A): string {
        return this.uniqueToA;
    }
}

class B {
    public uniqueToB = "b";

    public getString(this: B): string {
        return this.uniqueToB;
    }
}

function newAOrB () : A|B {
    return new A();
}
const aOrB : A|B = newAOrB();

function f(model: A | B): string {
    model.getString = aOrB.getString;
    return model.getString();
}

const b = new B();
const result = f(b);

//Expected: `"b"`
//Actual  : undefined
console.log(result);

Playground

RyanCavanaugh commented 5 years ago

Three options:

  1. This is working as intended
  2. Some line of @AnyhowStep's example should be an error
  3. Something else I haven't thought of should happen
jack-williams commented 5 years ago

I'm not entirely sure raising an error here is correct. At least, according to this work on intersection and union types , an evaluation context that consumes a union type is well typed if it's well typed for both types independently. It seems like the evaluation context:

x.getString() // x is the evaluation context were something of A or B may be placed

is well type if you assume x has type A, or if x has type B.

The example by @AnyhowStep is unsound because the property update is unsound, not the call.

Looking back at the motivation example from the PR #31547, there is a difference which is that the union of functions comes from the property itself, not from a synthesis of a union of objects.

So I wonder if the correct thing here is to not intersect this types for calls to synthetic unions obtained from reading a property.

In other words: you typically need to convert a union to an intersection when the context that introduces the union is different from the context that consumes the union---you have no assurances about which branch is chosen. In examples like the OP, the context that produces the union is similarly responsible for providing the this used to consume the union, so the producer and consumer are really the same thing.