microsoft / TypeScript

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

Signatures of intersection/union types properties #9239

Open iskiselev opened 8 years ago

iskiselev commented 8 years ago

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 commented 8 years ago
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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

awinogradov commented 5 years ago

Hi there! Any progress here?

TylorS commented 4 years ago

Hey all - I just wanted to add a simplified example of where this is currently failing for me as well.

https://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAYgdlAvFAFCghgLigZ2AJwEs4BzASiQD5cDjyoAyVDbOAVwFsAjCfCxau268KAelFQA8m3y4QeCByhs4AE15RgEPAChQkKAAkI6VQEF8JADxnqyM1AgAPLWpyp82YgDMNAJQAaKAA6UPxtYGx0OBAAbQBdfmpokCgAfig-KFYIADcNKHEoAHF8dC5NAAtob0J8PCh0S04IOGAdPXBoACEkIxNzSyt4aiLJOAAbVKEefCCuNmBcSoB7NgnVKB4oACI8IlIoAB8oGd4dzoMAYT6-CGAZOAAVLuG4UYlxqdPOWfnF5ZrDZbaB7WiHE5nfAXHRFboA6rhWAIQjuPCECYTRpcCbQYArEFQADG6ExEE2AHdCMBKlsVjSaAcSI01D9hPgdOoiRMmtAiSs4A1vNh4FAOt4UAByaqYlaSsg6CUARgVQA