-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Rules for intersection types are inconsistent with rules for implementing multiple interfaces. #4278
Comments
Or, alternatively, drop the intersection concept altogether (which is in practice just an unnecessarily complex and abstract reinvention of multiple inheritance with many caveats, subtle incompatibilities, and limited to the [somewhat narrow] scope of non-strict types - that are not very intuitive for programmers or generally easy to reason about) and instead simply define an operator based on inheritance itself - call it, say, the "type extension" operator: interface A extends B, C, D, E {}
// Define an equivalent operation:
type A = B extend C extend D extend E; For maximum usability, it would need to be more permissive about cycles than regular inheritance, though (every operand would only be evaluated once to be added to the resulting type - this also includes indirect self references between the operands themselves deeper in the chain): interface A extends A, B, B, C, D, E, A {} // Error: self reference of A and repetition of B.
type A = A extend B extend B extend C extend D extend E extend A; // OK! self references and repetitions are ignored. (the operator signifier doesn't have to be a word, of course - a symbol could be used instead for brevity) [The intersection feature it is not currently used in production code and not even released in Beta, so it's still not too late to modify its logic / replace it entirely] |
The biggest difference (and one that is continually lost in the discussion) between intersection types and Now, the ability for the operands to be type parameters also deeply affects the (possible) semantics of the operator. In particular, the operator has to consistently work for any two operands with an unknown set of members because it isn't possible to meaningfully report errors during type instantiation (i.e. when real types are substituted for type parameters). This what gives rise to the differences between So, it isn't a goal for |
Intersection types are great, certainly don't change them! I'll try to write up a more concrete proposal for how I'd like These are two situations where (I think also in a way which would be useful to the programmer - certainly to me - I only got on this topic because I encountered an issue with being unable to implement a class type with a protected member). @rotemdan , intersection types are available in 1.6 preview interface Emitter{
on(event, fn);
once(event, fn);
off(event, fn);
emit(event);
}
function MakeEmitter<T> (t: T): T&Emitter{
// add the functions to the object
require('emitter-library').mixin(t);
return <T&Emitter>t;
}
// It's a Widget, and it's an Emitter
var emitterWidget : Widget & Emitter = MakeEmitter(GetWidget());
|
Proposal: Where an implementing class or extending interface inherits a member of the same name from two or more base types (extended class or implemented or extended interfaces) the following rules should apply. If the member is optional in all base types, or required in all, and is public in in all, and has the same type, there is no error. (This is the current situation). Otherwise, if the member is not re-declared in the implementing class or extending interface, an error is produced. (this is the current situation). Otherwise, if the member is re-declared in the implementing class or extending interface, the following requirements must be met, or an error is produced.
Examples // Example of rule 1
interface A { m1: string;}
interface B { m1?: string;}
interface C extends A, B {
m1: string; // MUST be declared as required, because required in A
}
// example of rule 2 and rule 3
class J { protected _m: any; }
class K { private _m: any; }
class L { public _m: any; }
class M extends J implements K {
protected _m: any; // MUST be declared as protected as is protected in J
}
class N extends L implements J, K {
public _m: any; // MUST be declared as public as it is public in L
}
// example of rule 4
interface P { m2: Widget; }
interface Q { m2: Trimbler; }
interface R extends P, Q {
// MUST be redeclared to resolve incompatible types.
m2: Widget&Trimbler;
} Discussion for rule 4: a) If the property is read only and set by the constructor, the approach is obvious. b) if the property is read-write, you might accept either a Widget or a Trimbler and use application-specific logic to produce a |
The issues you describe seem very technical in nature and don't seem to suggest a fundamentally conceptual conflict between inheritance, or the wider concept of extension (which is conceptually more permissive with regards to things like cycles and member redeclaration - as it doesn't suggest a need to be bound to a well defined inheritance chain), and the scenarios you describe. If the programmer's intention is simply to extend types, then they should use the simplest operation possible that fulfills that task, and preferably one that's actually designed and advertised for that use - both in theory and in practice, not something else. It's also generally preferable to have that operation work on the widest possible range of types (e.g. strict types, even if not planned or really that needed that much anymore [I personally don't find them that important anymore if object literal and property assignments are strict], but they may possibly still be desirable, say, in forks. The point here is more about wisdom, not utility). Even if there are problems with adapting existing inheritance logic to the new scenarios, my preferred approach would be to use multiple inheritance as a starting point and diverge as much as needed, in a controlled and self-conscious way, (e.g. the modifications I mentioned to cycles and errors) and then arrive at a place where it would be useful for the target scenarios. It was not my goal to achieve the same semantics for As I've said, I cannot help address the issues mentioned because I do not completely understand the context or the type of challenges you have in mind. I mentioned the idea of having different "switches" in the compiler that could be turned on and off for different features, error levels, or strictness levels. Other than that, I can't really do anything more. As a side note: even without the consideration of strict types there's another point of dissonance between intersection and inheritance/extension, which I didn't mention so far - it has to do with hybrid object/function types (they are pretty weird but they do exist in Javascript): interface Func {
(a: number) => string
}
interface Obj {
b: boolean
}
interface HybridInterface extends Func, Obj {} // Works!
type HybridIntersection = Func & Obj // Math doesn't allow - would probably error or resolve to undefined. |
I haven't yet read the details of your proposal but wanted to mention that if your suggestion is to match the logic of an operation called "intersection" to that of multiple inheritance of interfaces - there's no reason in essence that would be (completely) possible while staying loyal to its theory and mathematical foundations . These are two different concepts based on two different conceptual frameworks: "intersection" is an operation defined on the sets of values satisfying certain types (which must be compatible, i.e. of the same superset to have any mathematical meaning). While inheritance/extension is a conceptually simpler and more intuitive operation defined simply as being the accumulation (or union, through some way that's not required to be bound to an underlying mathematical construct) of the properties (or more generally, the type specifiers) themselves. Like I mentioned on the side note, it is not possible to perform "intersection" on both an object and a function type, but definitely possible to create hybrid object/function types using inheritance (however weird it seems). Intersection does not result in anything useful when the operands are strict. Strict types, as far as I know, are not currently a planned feature in TypeScript - (though they were actually considered at one point - back a month ago that I wrote the message I linked to), but very popular and central to static languages. For example: strict interface A {
x: boolean
}
strict interface B {
y: boolean
}
interface Casual extends A, B {} // Works, of course! would be equivalent to { x: boolean, y: boolean }
type Disaster = A & B // undefined! Corresponding sets of satisfying values and their intersection:
There is no way to ensure consistency (both in the present or in future developments of the language) while having an intermingling of two different conceptual frameworks. The alternative I suggested (or some more complex adaptation of it addressing the challenges presented by @ahejlsberg ) would resolve this issue, have the same level of usefulness you now ascribe to intersection (or even more) and most of the time would give exactly the same result (where the deviations would probably be more, rather than less consistent with the rest of the language). |
@rotemdan I think you've made a small mistake in your example, since these are not strict types. Once you work through that I think you will see that, other than the syntax, there is probably very little difference between your proposal and what has actually been implemented in intersection types (available in typescript@next if you want to try them out). I'd be happy to continue the discussion by email if you wish because I am not sure this is the place for it. Explanation of intersection types: To be a member of type To be a member of In your example the set
Specifically, a value can have both a Boolean property The set
Therefore the intersection of the sets Likewise, I am a member of the intersection type |
My example was given for a hypothetical variation of an the interface type including the "strict" modifier ( Having both interface properties (which based on my tests are already applied strict type checking but I don't remember this was the case in earlier alpha versions of TypeScript) and now literals checked in a strict way (though it is still possible to assign non-member properties with square brackets - There is another problem with hybrid types that wasn't really mentioned yet. A hybrid type cannot be an operand to intersection in any case because it does not belong either to the set of all objects or the set of all functions (does not conform to any of the supersets). So: interface HybridInterface {
(param: string): boolean;
x: number;
}
type ImpossibleType = HybridInterface & AnyObjectOrFunctionType // = undefined or error regardless of what the type of |
@rotemdan, In that case can you please make a new issue for it as it doesn't belong here. |
WorkaroundsI didn't notice this but Rule 4 works currently in typescript@1.6. The other two are annoying but there are workarounds - essentially declare an throwaway interface which modifies the member. Unfortunately these don't work with Workaround 1: Base type members differ by optional/required statusProblem: It is not possible to extend or implement two interfaces, where a shared member differs in that it is optional in one and required in another: // Example of rule 1
interface A { m1: string;}
interface B { m1?: string;}
// not allowed, TS2320, Named property 'm1' of types 'A' and 'B' are not identical.
// ERROR: interface C extends A, B {}
// Workaround: Create an intermediate interface which
// modifies the optional member to be required.
interface B_x extends B { m1: string; }
interface C extends A, B_x {} Workaround 2: Base type members differ by public/protected status.Problem: You cannot implement (as an interface) a base type which has protected or private members. Note 1: when doing this you can only make the member public, you cannot make it protected, even if it was protected in both base classes. class P { protected m1: number; }
class Q { m1: number; }
// allowed, m1 is public
type PQT = P&Q;
var p : PQT = <PQT>{m1:undefined};
p.m1; // allowed, m1 is public in PQT
// not allowed
// TS2420: Class 'PQ' incorrectly implements interface 'Q'
// Property 'm1' is protected in type 'PQ' but public in type 'Q'.
// ERROR: class PQ extends P implements Q {}
// TS2420: Class 'QP' incorrectly implements interface 'P'.
// Property 'm1' is protected but type 'Q' is not a class derived from 'P'.
// ERROR: class QP extends Q implements P {}
// Allowed to relax protection explicitly in a derived class
class PQX extends P implements Q { m1: number;}
// Not allowed - again, seems a bug.
// ERROR: class QPX extends Q implements P { m1: number;}
// Workaround is to use an intermediate class to relax the protection
class PX extends P {m1: number;}
class QPX extends Q implements PX { m1: number;} Workaround 3: Base type members differ by type*Note: * This actually works currently in typescript@1.6. Where implementing a class which must derive from two base types, having the same member with different types, you can use a covariant property. // example of rule 4 above
interface Widget {w: number;}
interface Trimbler {t: string;}
interface R { m2: Widget; }
interface S { m2: Trimbler; }
interface T extends R, S {
// Must be redeclared to resolve incompatible types.
m2: Widget&Trimbler;
} |
It seems like the rules for a) implementing multiple interfaces and b) intersection types should be consistent.
Example 1: Optional members
Example 2: Protected members
(Surely from a strict point of view Q above is a subtype of P, because it can be used wherever P is used. All members visible externally on P are visible externally on Q, and all members visible internally on P are visible internally on Q. So I think that makes Q a subtype of P.)
Implementing two or more interfaces should create the same type as the intersection of the types. That they don't seems like a bug.
In pseudocode, this:
Should be identical in effect to this (if it was a thing):
The text was updated successfully, but these errors were encountered: