-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
It is already possible to redefine members in a sub-interface with 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 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" ( 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 |
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. |
I think what you'd want is:
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) |
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 Would this seem correct?
|
Because no value can be both a |
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. |
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. |
@danquirk So 'sub union type' in my mind is two-fold:
Maybe we don't have to introduce 'sub union type', instead just overload operator |
@duanyao can you provide an example where this type operator would be useful, and how is that any safer than |
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 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 A real world example is A = HTMLInputElement, B = HTMLProgressElement. They have member |
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 I have not seen any convincing scenarios in this thread so far that motivates changing what extends mean for an interface. |
Because union type is not allowed when redefining a member inherited from super interfaces:
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. |
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 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 |
interface A { value: string; }
interface B { value: number; }
interface AB0 extends A, B { value: ... }
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 I think we have two options here:
|
A few questions, if any of you wouldn't mind taking the time to answer them:
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. |
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.
yes. the only check here is that an interface is expected to be a subtype of its base types. that matches the intuition in in the example above,
this is an type operation that is not supported. we are looking into adding a mixin/intersection operator, that would serve in this case. |
Thanks @mhegazy . |
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'. |
Let's use:
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:
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.
The text was updated successfully, but these errors were encountered: