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

Create intersection types when interface members conflict. #3375

Closed
ttowncompiled opened this issue Jun 4, 2015 · 19 comments
Closed

Create intersection types when interface members conflict. #3375

ttowncompiled opened this issue Jun 4, 2015 · 19 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@ttowncompiled
Copy link

Let's use:

interface A = {
   data: number;
}
interface B = {
  data: string;
}
interface C extends A, B {}

This will throw an error as data has two separately defined types. I think that it would be really useful to allow C to become a union of the two interfaces either implicitly or to allow explicit unions of the conflicting types. The ultimate result being that:

interface C extends A, B {} = { data: number | string; }

Otherwise, a new interface would have to be created which would be a duplication of effort or the type would have to be overridden with any, which defeats the purpose of having strict types.

It's just a thought, but since TypeScript allows multiple inheritance with interfaces and has union types, I thought that this could be a beneficial merging of the two ideas.

@duanyao
Copy link

duanyao commented Jun 4, 2015

It is already possible to redefine members in a sub-interface with any type:

interface A {
  data: number;
}
interface B {
  data: string;
}
interface C extends A, B {
  data: any;
}

or with a sub-type:

interface A {
  data: Text;
}
interface B {
  data: Comment;
}
interface TextOrComment extends Text, Comment {}
interface C extends A, B {
  data: TextOrComment;
}

However it is not possible to redefine members as a union type:

interface A {
  data: number;
}
interface B {
  data: string;
}
interface C extends A, B {
  data: string | number; // error
}

Because string | number is not assignable to string or number.

I was also hit by this problem in https://github.com/duanyao/typescript-fluent-dom . I think what we want is a new concept of "sub union type" (||) which is assignable to its component types. E.g.:

var a: number;
var b: string;
var c1: number | string; // union type
var c2: number || string; // sub union type
a = c1; // error
a = c2; // ok
c2 = a; // ok

@ttowncompiled
Copy link
Author

Thanks for responding so quickly.

I think a sub union would be a great idea. We've already had a number of cases where we've had to override types with any and do typeof checks because we need the same object to adhere to different similar sets of types under separate contexts.

I've been thinking about creating a set of interfaces, but I was just going to run into the same problem and have to set the interface property types to any.

Sub unions would go a long way to alleviating these concerns and putting us back in a place where we can start using strict types again.

@jbondc
Copy link
Contributor

jbondc commented Jun 4, 2015

I think what you'd want is:

interface A {
  data: number;
}
interface B {
  data: string;
}
interface C mixin A, B {
  data: string | number;
}

So you're not inheriting but mixin in A & B in that order.

Since data is already in C, it checks that A.data and B.data is assignable to C.data (string|number)

@danquirk
Copy link
Member

danquirk commented Jun 4, 2015

What you want is for data to be an intersection, not a union (something like #1256). Consider:

interface A {
    data: number;
}
interface B {
    data: string;
}
interface C extends A, B {
    //data: string | number;
    data: any;
}

var a: A = { data: 1 };
var c: C = { data: "bye" };
a = c;
a.data.toFixed() // no compiler error, runtime exception

data in C is a string AND a number, not a a string OR a number.

@danquirk danquirk changed the title (suggestion) Create union types when interfaces conflict. Create intersection types when interface members conflict. Jun 4, 2015
@danquirk danquirk added the Suggestion An idea for TypeScript label Jun 4, 2015
@jbondc
Copy link
Contributor

jbondc commented Jun 4, 2015

@danquirk Would this seem correct?

interface A {
  data: number;
}
interface B {
  data: string;
}
interface C mixin A, B {
}
C.data; // number & string (intersection by default)

interface C2 mixin A, B {
  data: string | number;
}
C.data; // number | string (union) cause C2 wins

interface C3 mixin A, B {
  data: string & number;
}
C.data; // number & string (intersection) cause C3 wins

@duanyao
Copy link

duanyao commented Jun 5, 2015

@danquirk

data in C is a string AND a number, not a a string OR a number.

Because no value can be both a string and a number, it seems nothing except any can be assigned to C::data, which in turn makes nothing except any can be assigned to variable of C. This is why I suggested "sub union type (||)", which makes string || number and string assignable in double way; however I'm not sure if this is doable.

@danquirk
Copy link
Member

danquirk commented Jun 5, 2015

Pick a more comprehensible intersection type if that makes it clearer:

interface A {
    data: { x: number, y: number };
}
interface B {
    data: { x: number; z: number };
}
interface C extends A, B {
    // not ok
    //data: { x: number; y: number } | { x: number; z: number }

    // ok, this is something like the intersection of A.data and B.data
    data: { x: number, y: number, z: number }
}

The 'sub union type' you seem to be describing is this intersection type that has the members of both constituents (instead of only the members that exist in both like what a union means today). That's the only way it would make sense to allow the assignability relation between instances of A and C for example. Right now you have to manually describe this type when implementing conflicting interface members and you're not protected from a refactoring of A and B's members in the same way you would if there was an operator to combine the 2 types.

@ttowncompiled
Copy link
Author

If it's possible, I think that both intersection and sub-union could be equally usable. After bringing up intersection, it is clear to me that any implicit type inference on the compiler's part could be very confusing to a developer so an explicit union or an explicit intersection should be required.

In most cases, it seems that any would make more sense than intersection since the intersection of most types wouldn't make sense. There are examples like the one above though where intersection would be needed, though in that case, it's more like intersection is being used as a recursive call to resolve another instance of multiple inheritance.

If possible, intersection could serve as a very useful meta-type. An example would be the intersection of number and string. If data were assigned the value '2', then data is both a string and can be resolved to a number.

@duanyao
Copy link

duanyao commented Jun 6, 2015

@danquirk
If I understand it correctly, primitive types are completely different from interface types. Intersection of 2 interfaces is equivalent to a sub interface of them; however, primitive types can't be inherited.

So 'sub union type' in my mind is two-fold:

  • If A and B are both interface types, A || B is equivalent to intersection of A and B (or sub interface of of A and B).
  • If one of A and B is primitive type, A || B means both intercetion and union of A and B. This sounds contradictory, like "wave-particle duality" -- yes it is. For example, if variable c is of type string || number, then it acts as string & number on read, and acts as string | number on written. Thus string || number and string are assignable to each other. We can also view 'sub union type' of primitive types as a constrained verision of any.

Maybe we don't have to introduce 'sub union type', instead just overload operator & for primitive types.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 6, 2015

@duanyao can you provide an example where this type operator would be useful, and how is that any safer than any?

@duanyao
Copy link

duanyao commented Jun 7, 2015

Suppose we have these interfaces:

interface A { value: string; }
interface B { value: number; }
interface AB1 extends A, B { value: string || number; }
interface AB2 extends A, B { value: any; }

Code below shows that "sub union type(||)" is safer than any and has better tooling support:

var a : A;
var ab1: AB1 = { value: "12" } // ok, on write, value is string | number
a = ab1; // ok, on read, value is string & number
ab1 = { value: true } // error, on written, value is string | number
var ab2: AB2 = { value: true } // ok, but unexpected
ab2 = { value: "15" } // ok

ab1.value.substring(1); // ok, and IDE can autocomplete;
ab1.value.subString(1); // error, no such method on string or number
ab1.value.substring(1); // ok, but IDE can't autocomplete;
ab2.value.subString(1); // ok, but unexpected

ab1.value.toFixed(1); //ok, but throws at runtime
ab2.value.toFixed(1); //ok, but throws at runtime

Developers should be sure of the actual type of ab1.value, to prevent errors like ab1.value.toFixed(1).

A real world example is A = HTMLInputElement, B = HTMLProgressElement. They have member value of types string and number, respectively. I encountered this issue when creating UniversalElement (#3304).

@mhegazy
Copy link
Contributor

mhegazy commented Jun 9, 2015

So if the developer is expected to do a runtime check, why not use union types and type guards:

var stringOrNumber: number | string;

if(typeof stringOrNumber === "number" ) {
     stringOrNumber.toFixed(1); // does not blow up at runtime, and compiles fine.
}

I do not think there is value in creating another any, we already have one.

I have not seen any convincing scenarios in this thread so far that motivates changing what extends mean for an interface.

@mhegazy mhegazy added the Declined The issue was declined as something which matches the TypeScript vision label Jun 9, 2015
@mhegazy mhegazy closed this as completed Jun 9, 2015
@duanyao
Copy link

duanyao commented Jun 9, 2015

@mhegazy

So if the developer is expected to do a runtime check, why not use union types and type guards

Because union type is not allowed when redefining a member inherited from super interfaces:

interface A { value: string; }
interface B { value: number; }
interface AB1 extends A, B { value: string | number; } // error

I do not think there is value in creating another any, we already have one.

any has absolutely no type check and tooling support, but sub union type is different, as shown above.

But I must admit that sub union type, combined witn union type and intersection type, can introduce a lot of complexity.

I think this issue is caused by the tension between primitive types and interface types, and the tension between dynamic type and static type, which is hard to resolve.

@duanyao
Copy link

duanyao commented Jun 9, 2015

I just found a partial workaround for this issue: you need an additional "adapter" interface in the middle of the hierachy which redefines the conflicting member as any:

interface A { value: string; }
interface B { value: number; }
interface AB0 extends A, B { value: any; } // "adapter" interface
interface AB extends AB0 { value: string | number; } // here you can define the member with whatever type you like

// but AB is not compitable with A and B, a cast is needed
var a: A;
var ab: AB;
a = ab; // error
a = <A>ab; //ok

@kseo
Copy link

kseo commented Jun 9, 2015

interface A { value: string; }
interface B { value: number; }
interface AB0 extends A, B { value: ... }

interface AB0 extends A, B is never safe however we define the type of value.

  • value can't be string & number because there is no value that is both string and number simultaneously.
  • value can't be string | number because it violates the subtype relationship between AB0 and A (or B).

So both union types and intersection types are not a solution for this problem. What we need here is a way to override the type checker, but we don't just want to make it any because we already know that it is either string or number.

I think we have two options here:

  • Just stick to any.
  • Create a special construct to override the type checker in the way we want. For example, make it assignable to and from string or number, but treat it like any in other places. Sub union operator (||) suggested by @duanyao is one way to implement this. But this inevitably complicates the typing rule.

@ttowncompiled
Copy link
Author

A few questions, if any of you wouldn't mind taking the time to answer them:

  1. Is there currently a way to implicitly create an intersection of two interfaces? Or would an adapter have to be defined?
  2. Can a union type in a parent interface be constrained by a child interface?
  3. During compilation, is it not possible to assert the claim that a value is both a string and number?

Intersection types seems really interesting to me and from a compile standpoint, values could be checked if they satisfy the language spec for both a string and a number. Intersection types would also make it very easy to create types that extend multiple interfaces without having to create a namespace for it.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 11, 2015

Is there currently a way to implicitly create an intersection of two interfaces? Or would an adapter have to be defined?

No. but it is a structural type system, so you can define a new type that has a union type member and that would be assignable to both types.

Can a union type in a parent interface be constrained by a child interface?

yes. the only check here is that an interface is expected to be a subtype of its base types. that matches the intuition in interface Foo extends Bar that any Foo is a Bar.

in the example above, number | string is not assignable to number, but the opposite is true. so you can define interface hierarchy as such:

interface Base {
    value: number | string;
}
interface A extends Base {
    value: string;
}
interface B extends Base {
    value: number;
}

During compilation, is it not possible to assert the claim that a value is both a string and number?

this is an type operation that is not supported. we are looking into adding a mixin/intersection operator, that would serve in this case.

@ttowncompiled
Copy link
Author

Thanks @mhegazy .

@luchillo17
Copy link

What about conflicting access modifiers? say interface A has a required attribute, but in interface B it should be optional?

interface A {
  id: number;
}
interface B extends A {
  id?: number;
}
// Error: Property 'id' is optional in type 'B' but required in type 'A'.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

7 participants