Skip to content

Signatures of intersection/union types properties #9239

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

Open
iskiselev opened this issue Jun 18, 2016 · 6 comments
Open

Signatures of intersection/union types properties #9239

iskiselev opened this issue Jun 18, 2016 · 6 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@iskiselev
Copy link

Consider next example:

interface A{
    f:string;
    set_f(val:string) : void;
    get_f() : string;
}

interface B{
    f:number;
    set_f(val:number) : void;
    get_f() : number;
}

let n:number;
let str:string

let first : A&B;
first.f = n; // error, as f has type number&string. Probably OK, but probably it should be number|string, to be symmetric with set_f function.
first.f = str; // error, as f has type number&string.
n = first.f;
str = first.f;

first.set_f(n);
first.set_f(str);
n = first.get_f(); // error, typescript prefer to use first signature () => string. Shouldn't it be string&number.
str = first.get_f();

let second : A|B;
second.f = n; // Ops, no error! But it should be at least string&number here
second.f = str;// Ops, no error! But it should be at least string&number here
n = second.f; // error, typescript prefer to use first signature string. It should be string|number
str = second.f;// Ops, no error, typescript prefer to use first signature string. It should be string|number

second.set_f(n); //error, lack a call signature. It's really good, but better if it would be (string&number) => void
second.set_f(str); //error, lack a call signature. 
n = second.get_f(); // error, cool, typescript was able to correctly infer result as string|number
str = second.get_f();// error, cool, typescript was able to correctly infer result as string|number

With #9167 additional to previous was added preserving readonly flag for intersection. Should't readonly flag be reset if any of intersection member has no such flag? As intersection should extend possibilities of type, and never narrow down it.

@aluanhaddad
Copy link
Contributor

let second : A|B;
second.f = n; // Ops, no error! But it should be at least string&number here
second.f = str;// Ops, no error! But it should be at least string&number here
n = second.f; // error, typescript prefer to use first signature string. It should be string|number
str = second.f;// Ops, no error, typescript prefer to use first signature string. It should be string|number

second.f initially has type string | number so a value of type string, number, string | number, or string & number would be valid.

let first : A&B;
first.f = n; // error, as f has type number&string. Probably OK, but probably it should be number|string, to be symmetric with set_f function.

How would the language know that set_f is related to f?

n = first.get_f(); // error, typescript prefer to use first signature () => string. Shouldn't it be string&number.

The spec indicates that this behavior is correct, but I find it very counter-intuitive.
As an implementation of get_f could not differ in behavior based on its parameters (since it has none) it must theoretically return a value which satisfies both string and number which would be string & number. Instead what we end up with is 2 call signatures. Since the first call signature takes precedence, the signature returning number cannot be called.
Intuitively, I would expect the call the signatures to be combined into a single signature having the union of their parameter types and the intersection of their return types. But that is not what the spec says.

@iskiselev
Copy link
Author

iskiselev commented Jun 19, 2016

I understand, that current behavior is correct according to spec, but in given cases it's cont-intuitive and gives you an opportunity to make an error that type-system could find.
Let's look: A|B < A < A&B; A|B < B < A&B (according to ability for reference assignment).
As we know, function is contravariance to it's arguments and covariance to it's result type. Let's look on the sample once more with this statements:

first.set_f(n);
first.set_f(str);

Typescript found two overloads here. Really, it behaves same, as if it has signature (string|number)=>void, and it is great: string|number < string, string|number < number and A&B>A, A&B>B. So, argument is really contravariant here, and it is great!

second.set_f(n); //error, lack a call signature. It's really good, but better if it would be (string&number) => void
second.set_f(str); //error, lack a call signature.

It will be really hard to calculate correct sum of all overrides, and it's OK that it's not working. But let's try to calculate it in this simplest case: A|B < A, A|B < B. The only valid combination here may be string&number > string, string&number > number.

Now let's look on result type:

var r:number|string = second.get_f();

TypeScript greatly infer correct result type here, as A|B < A, A|B < Band string|number < string, string|number < number.

n = first.get_f(); // error, typescript prefer to use first signature () => string. Shouldn't it be string&number.
str = first.get_f();

Technically it's not correct: as A&B > A, A&B > B it should be string&number > string, string&number > number.

Now let's look on properties. Compiler for sure can't have any knowledge that f relates to get_f and set_f, but we understand it. It means that specification should be written with this knowledge in mind. For it, we should calculate property type different: it should be covariant when it is used as R-Value and contraviriant when it is used as L-Value. If it will be implemented, property could walk absolutely same as we'd want get and set methods work.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jun 19, 2016

I think that the way that property types are inferred in an intersection is intuitive, but that the way that nullary function types are inferred is not. The problem is, as you say,

It will be really hard to calculate correct sum of all overrides

However, when intersecting the types of nullary functions the overload set produced is really counter intuitive because they are distinguished solely by return type. So the type A & B would have a single get_f with the type () => string & number, just as the type A | B currently has a single get_f with the type () => string | number.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 18, 2016
@RyanCavanaugh
Copy link
Member

We're continuing to look for a good algorithm that can produce "correct" signatures from intersection types.

@awinogradov
Copy link

Hi there! Any progress here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants