Repository navigation
Rules for intersection types are inconsistent with rules for implementing multiple interfaces. #4278
Copy link
Copy link
Closed
Labels
By DesignDeprecated - use "Working as Intended" or "Design Limitation" insteadDeprecated - use "Working as Intended" or "Design Limitation" insteadDeclinedThe issue was declined as something which matches the TypeScript visionThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScriptAn idea for TypeScript
Description
It seems like the rules for a) implementing multiple interfaces and b) intersection types should be consistent.
Example 1: Optional members
interface A { m1: string;}
interface B { m1?: string;}
// allowed, m1 is required.
type CT = A&B;
var a : CT = {m1:undefined};// allowed, string can be undefined
// error, m1 is required,
//TS2322: Type '{}' is not assignable to type 'A & B', Property 'm1' is missing in type '{}'.
var b : CT = {};
// not allowed, TS2320, Named property 'm1' of types 'A' and 'B' are not identical.
interface C extends A, B {}Example 2: Protected members
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'.
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'.
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.
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 QPX2 extends Q implements PX { m1: number;}- Implementing multiple interfaces should create a type which is the intersection of the types of the interfaces (but with any new members added).
- It should not be an error to make an optional member non-optional in a derived class. Non-optional members can be undefined, so nothing should break. This is consistent with intersection types. (Arguably also vice versa.)
- It should not be an error to relax the protected or private status of a member in a derived or implementing class, consistent with intersection types. (in 1.6 you can do it in a derived class but not an implementing class). Or at any rate it should be possible to do so if you need to. See also Allow interfaces to declare protected members and allow implementors to implement protected and private members. #3854
(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:
interface C extends A, B {}Should be identical in effect to this (if it was a thing):
type C1 = A & B;
interface C extends C1 {}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
By DesignDeprecated - use "Working as Intended" or "Design Limitation" insteadDeprecated - use "Working as Intended" or "Design Limitation" insteadDeclinedThe issue was declined as something which matches the TypeScript visionThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScriptAn idea for TypeScript
Activity
rotemdan commentedon Aug 12, 2015
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:
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):
(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]
ahejlsberg commentedon Aug 12, 2015
The biggest difference (and one that is continually lost in the discussion) between intersection types and
extendswith multiple interfaces is that the intersection type operator&permits the operands to be type parameters. More details here. This ability adds important new expressiveness and is the key motivator for the feature. Indeed, if it wasn't for this ability we wouldn't be implementing intersection types in the first place, because you could just as well create the new types by inheriting from multiple interfaces.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
&andextends. Because the actual types are always known withextendswe can do more checking and forbid constructs that we deem wrong from a classical OOP perspective. For example, we can error when properties of the same name but different types conflict withextends, whereas we merge (i.e. intersect) their types with&.So, it isn't a goal for
&andextendsto have identical semantics and it isn't clear that it ever could be. Of course I'm perfectly happy to consider proposals that would bring them closer, but with the constraint that&needs to work with type parameters. Otherwise it simply isn't interesting.benliddicott commentedon Aug 12, 2015
Intersection types are great, certainly don't change them! I'll try to write up a more concrete proposal for how I'd like
implementsto change.These are two situations where
implementscould be made more consistent with intersection types, rather than the other way around, specifically by relaxing some constraints onimplements.(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).
Rotem Dan (@rotemdan) , intersection types are available in 1.6 preview
npm install typescript@nextfor the nightly builds. Try them, you'll like them.benliddicott commentedon Aug 12, 2015
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
Discussion for rule 4:
Ideally the rule would be to make the property covariant so assignment accepted the union type (either a
Widgetor aTrimbler), but retrieval returned the intersection type (Widget&Trimbler), but this can't be done automatically. Clearly the programmer must resolve this on a case-by-case basis.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
Widget&Trimblerif required. c) quite commonly it won't actually matter as only a few properties which the types have in common will actually be used, in which case a cast is all that is needed.rotemdan commentedon Aug 12, 2015
Anders Hejlsberg (@ahejlsberg)
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
extends(I don't even know if that would be useful), but a arrive at a consistent one that's easy to explain through listing a number of "tightenings" and "relaxations" in the logic.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):
rotemdan commentedon Aug 12, 2015
benliddicott
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:
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 Anders Hejlsberg (@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).
benliddicott commentedon Aug 12, 2015
Rotem Dan (@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
{x: Boolean}the value must have a Boolean propertyx. But it can have additional properties too. (I am a member of typeTwo-Legged-Thingseven though I also have arms, and intestines.)To be a member of
{y: Boolean}the value must have a Boolean propertyy. But it can have additional properties too. (I am a member of typeTwo-Armed-Thingseven though I also have legs and lungs.)In your example the set
Val(A)does not have just two entries, it is infinite. Anything which has{x: Boolean}is a member of the set no matter what additional properties it has. It can have any number of additional properties, of any type and any value, and still be a member of the set provided it has a Boolean `x'Specifically, a value can have both a Boolean property
x, making it a member of type{x: Boolean}and also a Boolean propertyy, making it a member of type{y: Boolean}.The set
Val(A)therefore also includes:Therefore the intersection of the sets
Val(A)andVal(B)includes everything which has a BooleanxAND a Booleanyregardless of what additional properties it has. So it's still an infinite set. The only values which are excluded fromVal(A) n Val(B)are those which have no Booleanxor which have no Booleany.Likewise, I am a member of the intersection type
Two-Legged-Things & Two-Armed-Things. Having lungs and intestines does not disqualify me. I would also be a member of "Toothed-Things" since I have all my own teeth too.rotemdan commentedon Aug 13, 2015
benliddicott
My example was given for a hypothetical variation of an the interface type including the "strict" modifier (
strict interface A) that would, actually be that defined that way - similar to, say, structs in C:struct A { int x }. I'm aware that TypeScript interfaces are non-strict and would work the way you describe. I mentioned that introducing these kinds of types was considered at some point, but then abandoned (if I understood correctly) in favor of adding strict object literal assignment checking.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 -
x["abc"] = 3, or add[x: string]: anyto the interface that would essentially "switch off" the strict literal feature) would make the types in effect "feel" strict for most cases so non-strictness would mostly be experienced when assigning two objects given well-defined types that are structurally sub/super-types of each other. I don't think that is really a concern for human error but I'm not 100% sure as I haven't really used the feature in practice.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:
regardless of what the type of
AnyObjectOrFunctionTypeis .benliddicott commentedon Aug 13, 2015
Rotem Dan (@rotemdan), In that case can you please make a new issue for it as it doesn't belong here.
benliddicott commentedon Aug 14, 2015
Workarounds
I didn't notice this but Rule 4 works currently in [email protected]. The other two are annoying but there are workarounds - essentially declare an throwaway interface which modifies the member. Unfortunately these don't work with
private(see #3854) - there is no escape hatch for that. Recommendation: Don't useprivate- useprotectedinstead (unless you hate your downstream devs).Workaround 1: Base type members differ by optional/required status
Problem: 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:
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.
The workaround is to derive directly from it, and modify the protection to public, then implement that.
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.
Note 2: This does not work with
private. I haven't found a workaround for that.Workaround 3: Base type members differ by type
*Note: * This actually works currently in [email protected].
Where implementing a class which must derive from two base types, having the same member with different types, you can use a covariant property.