Skip to content
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

Intersection type of two array types does not widen in callback #11961

Closed
NoelAbrahams opened this issue Oct 31, 2016 · 4 comments
Closed

Intersection type of two array types does not widen in callback #11961

NoelAbrahams opened this issue Oct 31, 2016 · 4 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@NoelAbrahams
Copy link

Hi, Guys,

Something a bit strange here:

TypeScript Version: 2.0.6

Code

    type foo1 = { id: number}[];
    type foo2 = { id: number, value: string }[];
    type foo3 = foo1 & foo2;

    var x: foo3;
    x[0].value; // okay
    x.forEach(item => {

        // Error: Property 'value' does not exist on type '{ id: number; }'.
        item.value; 
    });

Expected behavior:

No error when property value is referenced in the body of the forEach.

Actual behavior:

The compiler reports an error.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Oct 31, 2016
@RyanCavanaugh
Copy link
Member

This happens because we just merge the signatures of forEach in order, whereas element access produces the recursively merged property type. You could specify a type annotation on item, or write type foo3 = foo2 & foo1 (!).

But in general it's really unclear how type TU = T[] & U[] should be interpreted -- is it a heterogeneous array, the same as Array<T | U> ? Or the same as Array<T & U> ? An axiom is that for any operation x.y(z), that should still be legal for x if x becomes an intersection type containing the original constituent, but that wouldn't be true if we interpreted the array to be Array<T & U> (since z would have to be the intersected element type as well).

So overall... probably a bad idea to have the type T[] & U[] floating around.

@NoelAbrahams
Copy link
Author

@RyanCavanaugh,

or write type foo3 = foo2 & foo1.

That's really weird behaviour, but useful to know.

This also highlights another difference in using interfaces vs type definitions:

interface foo1 {
    val: { id: number }[];
};

interface foo2 extends foo1 {
    val: { id: number, value: string }[];
};

var x: foo2;

x.val.forEach(item => {

    item.value; // Now okay 
});

I suggest that type foo2 = foo1 & { } work the same as interface foo2 extends foo1{ }.

@simonbuchan
Copy link

simonbuchan commented Jan 12, 2018

@RyanCavanaugh, I'm not sure it follows from that that X[] & Y[] is not a well behaved type, since the axiom statement you give explicitly assumes x and z are the same type.

Lets get rid of Array weirdness and callback type inference:

function f<X, Y>(x: X, y: Y) {
    type Box<T> = {
        get(): T;
        set(v: T): void;
    };
    let xAndY: X & Y, xOrY: X | Y;
    let xBox: Box<X>, yBox: Box<Y>;
    let xAndYBox: Box<X & Y>;
    let xOrYBox: Box<X | Y>;
    let xBoxAndYBox: Box<X> & Box<Y>;

    xBox.set(x);
    xBox.set(y); // Error: Y is not X
    xBox.set(xAndY);
    xBox.set(xOrY); // Error: X | Y is not X
    x = xBox.get();
    y = xBox.get(); // Error: X is not Y
    xAndY = xBox.get(); // Error: X is not X & Y
    xOrY = xBox.get();

    yBox.set(x); // Error: X is not Y
    yBox.set(y);
    yBox.set(xAndY);
    yBox.set(xOrY); // Error: X | Y is not X
    x = yBox.get(); // Error: Y is not X
    y = yBox.get();
    xAndY = yBox.get(); // Error: Y is not X & Y
    xOrY = yBox.get();

    xAndYBox.set(x); // Error: X is not X & Y
    xAndYBox.set(y); // Error: Y is not X & Y
    xAndYBox.set(xAndY);
    xAndYBox.set(xOrY); // Error: X | Y is not X & Y
    x = xAndYBox.get();
    y = xAndYBox.get();
    xAndY = xAndYBox.get();
    xOrY = xAndYBox.get();

    xBoxAndYBox.set(x); // OK? but `yBox.set(x)` is an error.
    xBoxAndYBox.set(y); // OK? but `xBox.set(y)` is an error.
    xBoxAndYBox.set(xAndY); // OK
    xBoxAndYBox.set(xOrY); // Error
    x = xBoxAndYBox.get(); // Ok? but `x = yBox.get()` is an error
    y = xBoxAndYBox.get(); // Error, but & is asymetrical!
    xAndY = xBoxAndYBox.get(); // Error
    xOrY = xBoxAndYBox.get(); // OK

    // In summary: . = ok, ! = error
    //  set  get
    // .!.! .!!. : Box<X>
    // !..! !.!. : Box<Y>
    // !!.! .... : Box<X & Y>
    // .... !!!. : Box<X | Y>
    // ...! .!!. : Box<X> & Box<Y>
    // A is a B iff A has all .'s B has.
    xBox = xBoxAndYBox; // OK
    yBox = xBoxAndYBox; // OK? but y = yBox.get() is ok, while `y = xBoxAndYBox.get()` is an error
    xAndYBox = xBoxAndYBox; // Error
    yOrYBox = xBoxAndYBox; // Error
    xBoxAndYBox = xBox; // Error
    xBoxAndYBox = yBox; // Error
    xBoxAndYBox = xAndYBox; // OK? but `xBoxAndYBox.set(x)` is ok, while `x = xAndYBox.set(x)` is an error
    xBoxAndYBox = yOrYBox; // Error
}

Clearly the type algebra is wonky here! But by combining acceptable uses of Box<X> and Box<Y>, you should get a Box<X | Y> when used covariantly (get), but Box<X & Y> when used contravariantly (set).

Back to arrays, the typesafe behavior logically should be:

const catdogs: Cat[] & Dog[] = ???;
const dogs: Dog[] = catdogs;
const cats: Cat[] = catdogs;

dogs.push(new Dog());
cats.push(new Cat());
// catdogs now has a Dog and a Cat!

let dog: Dog, catdog: Cat & Dog, hysteria: Cat | Dog;
catdogs.push(dog); // Error: Dog is not a Dog & Cat, otherwise cats has a Dog.
catdogs.push(catdog); // OK: dogs only has Dogs, cats only has Cats
dog = catdogs.pop(); // Error: Cat | Dog is not a Dog, otherwise dog could get a Cat from cats.
catdog = catdogs.pop(); // Error: Cat | Dog is not a Cat & Dog, it could come from cats or dogs.
hysteria = catdogs.pop(); // OK: Could be from cats or dogs, but common is still accessible.

@simonbuchan
Copy link

For the curious, this is not an issue specific to overload sets, simple fields have the same issue:

interface Cat { meow(): void }
interface Dog { bark(): void }
interface Owner<Pet> { pet: Pet; }

declare let cat: Cat, dog: Dog, catdog: Cat & Dog, pet: Cat | Dog;
declare let petOwner: Owner<Cat | Dog>;
declare let catOwner: Owner<Cat>;
declare let dogOwner: Owner<Dog>;
declare let strangeOwner: Owner<Cat> & Owner<Dog>;

catOwner = strangeOwner;
dogOwner = strangeOwner;

if (Math.random() < 0.5)
    catOwner.pet = cat;
else
    dogOwner.pet = dog;

strangeOwner.pet = pet; // Error, otherwise catOwner could get a Dog, or dogOwner could get a cat
strangeOwner.pet = catdog; // OK

pet = strangeOwner.pet; // OK: Cat | Dog
catdog = strangeOwner.pet; // OK? but Cat | Dog is not Cat & Dog!
catdog.bark(); // boom if strangeOwner is a catOwner
catdog.meow(); // boom if strangeOwner is a dogOwner

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants