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

Rules for intersection types are inconsistent with rules for implementing multiple interfaces. #4278

Closed
benliddicott opened this issue Aug 11, 2015 · 10 comments
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@benliddicott
Copy link

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;}
  1. Implementing multiple interfaces should create a type which is the intersection of the types of the interfaces (but with any new members added).
  2. 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.)
  3. 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 {}
@rotemdan
Copy link

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]

@ahejlsberg
Copy link
Member

The biggest difference (and one that is continually lost in the discussion) between intersection types and extends with 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 & and extends. Because the actual types are always known with extends we 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 with extends, whereas we merge (i.e. intersect) their types with &.

So, it isn't a goal for & and extends to 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
Copy link
Author

Intersection types are great, certainly don't change them! I'll try to write up a more concrete proposal for how I'd like implements to change.

These are two situations where implements could be made more consistent with intersection types, rather than the other way around, specifically by relaxing some constraints on implements.

(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 npm install typescript@next for the nightly builds. Try them, you'll like them.

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());

@benliddicott
Copy link
Author

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.

  1. If the member is required in any base type, it must be declared as required in the implementing class or extending interface.
  2. If the member is public in any base type and protected or private in any base type, it must be explicitly declared as public in the implementing class or extending interface.
  3. If the member is not public in any base type, and is protected in any base type and is private in any base type, it must be explicitly declared as public or protected in the implementing class or extending interface.
  4. The member must be declared as a type which is compatible with all the types of the member in the base types. For example it might be the intersection of the types. For functions the set of signatures should be compatible with all of the signatures from all base types.

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:
Ideally the rule would be to make the property covariant so assignment accepted the union type (either a Widget or a Trimbler), 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&Trimbler if 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
Copy link

@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):

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.

@rotemdan
Copy link

@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:

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:

Val(A) = {{ x: false }, { x: true }}
Val(B) = {{ y: false }, { y: true }}

Val(A) ∩ Val(B) = { } // The empty set!

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).

@benliddicott
Copy link
Author

@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 property x. But it can have additional properties too. (I am a member of type Two-Legged-Things even though I also have arms, and intestines.)

To be a member of {y: Boolean} the value must have a Boolean property y. But it can have additional properties too. (I am a member of type Two-Armed-Things even 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'

Val(a) = { 
   {x: false}, {x: false, z: 1}, {x: false, z: 2}...
   (x: true}, {x: true, z: 1}, {x: true, z: 2}...
   (x: false}, {x: false, foo: "bar"}, {x: false, foo: "baz", ping: "pong"... }... 
...
}

Specifically, a value can have both a Boolean property x, making it a member of type {x: Boolean} and also a Boolean property y, making it a member of type {y: Boolean}.

The set Val(A) therefore also includes:

    {x: false, y: false}, {x: false, y:true}, 
    {x: true, y: false}, {x: true, y:true}, 
    {x: true, y: false, foo: "bar"}, {x: true, y:true, foo: "baz"}, ...

Therefore the intersection of the sets Val(A) and Val(B) includes everything which has a Boolean x AND a Boolean y regardless of what additional properties it has. So it's still an infinite set. The only values which are excluded from Val(A) n Val(B) are those which have no Boolean x or which have no Boolean y.

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
Copy link

@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]: any to 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:

interface HybridInterface {
    (param: string): boolean;
    x: number;
}

type ImpossibleType = HybridInterface & AnyObjectOrFunctionType // = undefined or error

regardless of what the type of AnyObjectOrFunctionType is .

@benliddicott
Copy link
Author

@rotemdan, In that case can you please make a new issue for it as it doesn't belong here.

@mhegazy mhegazy added By Design Deprecated - use "Working as Intended" or "Design Limitation" instead Declined The issue was declined as something which matches the TypeScript vision and removed In Discussion Not yet reached consensus labels Aug 13, 2015
@mhegazy mhegazy closed this as completed Aug 13, 2015
@benliddicott
Copy link
Author

Workarounds

I 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 private (see #3854) - there is no escape hatch for that. Recommendation: Don't use private - use protected instead (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:

// 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.
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.

    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;
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead 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

5 participants