Skip to content

Proposal: structural overloading for interfaces #4302

Closed

Description

@rotemdan

An alternative to extends that is mostly identical but with the following variation: instead of erroring when properties are re-declared with different types, it would resolve to the type unions instead (properties having a function type are already overloaded in inheritance, so there's no need to address them):

interface A {
    x: boolean;
}

interface B extends A { // Error! Incorrectly extends A. Types of x are incompatible.
    x: number;
}

interface C overloads A { // OK! x is now of type boolean | number
    x: number;
}

interface D overloads A { // OK! x is now of type boolean | number | string
    x: number | string;
}

interface E overloads A { // OK! x is now of type boolean | (param: number): string
    x(param: number): string;
}

An operator based on this variation of inheritance would have the property that for any two (object or function) interfaces A, B, A overload B (illustrative, not suggested, name) can never fail, even if the operands contain incompatible property types (note: this assumes cycles are ignored in the operator version). In contrast to intersection, it wouldn't even fail if A is an object type and B is a function type, as hybrid object/function types are already allowed by standard inheritance.

This an attempt to address the challenge presented by @ahejlsberg of having an inheritance-like operation that is guaranteed not to fail when applied to generic type parameters:

declare function combine<T, U>(obj1: T, obj2: U): T overload U;

and thus may be used as a replacement to intersection that would be more consistent with the rest of the language (and inheritance logic in particular).

Even though I'm not a computer scientist or a language expert I tried to do my best for giving a basic direction (that is, at least a possible idea) on how to solve this. I'm not in a position to be able to see all the details, nuances or problems for integrating something like this into the language.

Activity

  1. JsonFreeman commented on Aug 14, 2015

    @JsonFreeman
    Contributor

    Seems like there are two ideas here. One is a new kind of heritage clause that is slightly different from extends, but is similar in spirit. I think that would be feasible, but why would it be useful?

    The second idea is a type operator that takes any two types, and creates a new type that inherits from both operand types? But it is not obvious how this operator would work when the operands are type parameters, union types, primitives, etc (anything that is not an object type).

  2. rotemdan commented on Aug 14, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    For a start, I first wanted to consider what is the actual scope and character of the "overloads" heritage relation (not the operator). The first thing that I wanted to check is if this applies to anything else than interfaces (I'm including function and hybrid object/function types here, not just object types, I will demonstrate that later). Primitives (and possibly unions) would not fit of course, because they don't have an internal structure. In the cases of classes, it will violate OOP principles by allowing a derived type to have the same property of a different type than defined on a base type, so the base type cannot assure the type (or possible range of types) that the property is assigned to.

    Hypothetical (problematic) example:

    class Base {
        x: boolean;
    }
    
    class Derived overloads Base {
        x: number;
    }
    
    let derivedInstance = new Derived();
    derived.x = 53;
    
    let baseInstance: Base = derivedInstance;
    
    let expectedBooleanValue = baseInstance.x; // Expected: boolean type. Wrong! x is actually a number.

    So classes are also not really an application for this.

    Now even when constrained to the scope of interfaces. Should this actually be even described as inheritance? (at least with the analogy from classes?). I don't know, maybe it does, maybe not, so for caution, I changed the title to "structural overloading for interfaces", just to be on the safe side. This is all quite challenging for me, so I will try to address more of the presented issues later, like possible use cases (that I can definitely think of, this does describe a possible pattern that can appear in dynamic languages) and more about the operator I mentioned.

  3. changed the title [-]Proposal: overloading inheritance[/-] [+]Proposal: structural overloading for interfaces[/+] on Aug 14, 2015
  4. rotemdan commented on Aug 14, 2015

    @rotemdan
    Author

    An example of how this would work with two hybrid function/object interfaces (and also applies to cases where only one is hybrid, as well):

    interface A {
        (num: number): string;
        x: boolean;
    }
    
    interface B overloads A {
        (str: string): number;
        x: number;
    }
    
    interface StructurallyEquivalentToB { // Demonstrate B after the structural overloading is applied.
        (num: number): string;
        (str: string): number;
        x: boolean | number;
    }
  5. rotemdan commented on Aug 14, 2015

    @rotemdan
    Author

    Before attempting to go into the details of the operator, there is first a consideration of a theoretical issue with a class that implements an overloaded interface. This is more of a question for language experts. The question is:

    Consider a class that implements an interface (and doesn't add anything of its own), and another similar class that implements an interface that is hierarchically lower than the other one (I'm intentionally avoiding using the word "derived" here). According to the principles of OOP: Would the first class be automatically considered a super-type of the other one? For example:

    interface BaseInterface {
        x: number;
    }
    
    interface OverloadedInterface overloads BaseInterface {
        x: string;
    }
    
    class SomeClass implements BaseInterface {
        x: number = 53;
    }
    
    class SomeOtherClass implements OverloadedInterface {
        x: number|string = "Hello World!";
    }

    Is SomeClass expected a to be a super-type of SomeOtherClass? I mean, because of the "type expansion" done by overloading, It cannot guarantee that would be the case.

    Or alternatively, there's a simpler possible problem with this:

    interface BaseInterface {
        x: number;
    }
    
    interface OverloadedInterface overloads BaseInterface {
        x: string;
    }
    
    class SomeClass implements OverloadedInterface {
        x: number|string = "hello!";
    }
    
    let classInstance = new SomeClass();
    let overloadedInterfaceInstance = classInstance;
    
    // This assignment is *not* structurally correct, but may work because of their 
    // positions in the hierarchy. 
    let baseInterfaceInstance = overloadedInterfaceInstance;
    
    // According to the base interface, x is of type number, not string|number so this 
    // may incorrectly assume a wrong type.
    let x = baseInterfaceInstance.x; 

    If these are real problems, then overloads can not be used as an hierarchical relation, and I guess in that case I would concentrate on the operator (which probably wouldn't need to maintain hierarchies anyway - so that wouldn't be a problem).

  6. rotemdan commented on Aug 14, 2015

    @rotemdan
    Author

    First I would explain why I considered defining a hierarchical relation (which does seem to have its problems and may not turn out to be a good idea). It is not just to explore its use but also to try to consider on how to integrate the operation more strongly and consistently into the language (and observe things like OOP issues etc.). This may turn out to only be a hypothetical experiment (as the problems I showed seem possibly major), but I find it useful.

    The idea of the structural overloading operator is to try to capture the intuitive concept of "merging" types structurally, similarly to what is achieved by intersection, but without being bound to strict and abstract math thus more directly reflecting the intention of the programmer both in practice and theory. I didn't intend to define it for anything other than objects, functions and object/function hybrids. I currently don't feel it would be a good idea to enlarge its scope even further, say also to primitives or unions, but that might be a future area to explore.

    In the case of objects: the main purpose of the operation is to capture the type equivalent of object structural merging (with a logic similar to jQuery.extend, at least when the operation is not a "deep"/recursive one), for example:

    let a = {a: 54, b: "hello", c: true};
    let b = {b: 991, c: undefined}
    
    // Now merge b into a while avoiding copying undefined values:
    let result = {a: 54, b: 991, c: true}

    The equivalent type operation would reflect the possible values after the objects are merged:

    interface A {a: number, b: string, c: boolean}
    interface B {b: number, c: string}
    
    type Result = B overload A // results in {a: number, b: string|number, c: boolean|string}

    So in practice it might seem to behave exactly like intersection, at least for most cases. The major (and very important) difference it that it's 100% backwards compatible with inheritance (while the opposite is not true in general). For every two types that do not fail on:

    interface A extends B,C {}

    it would also not fail and derive the exact same type (and is additionally guaranteed never to fail if B and C are known to be object/function/hybrid interfaces):

    type A = C overload B; // Can also be written as B overload C - it is a commutative operation.

    The "overload" name is given just for illustration. I will try to think of a symbol that would make it more concise.

  7. JsonFreeman commented on Aug 14, 2015

    @JsonFreeman
    Contributor

    I think the key problem is the one you've mentioned in #4302 (comment). This problem exists for interfaces too. I don't think the assignment let baseInstance: Base = derivedInstance; from your example should be allowed because Derived is simply not assignable to Base. With the extends keyword, the assumption on the user's part is that the derived type will be assignable to the base, but with the feature being proposed, that assumption cannot be made.

    So I think what would be feasible is a heritage clause that says, "my new type is just like this old type, but with the following exceptions". It would not be guaranteed that the derived type is assignable to the base type. It may have no meaningful relation at all to the base type, but would just use the base type for inspiration.

    I still think it is more useful to combine the use of intersection types and "extends" than it is to introduce an operator that is half way in between.

  8. rotemdan commented on Aug 15, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    Yes I have detected and explained the problem myself in detail. It does not maintain assignment integrity that is consistent with the position in the hierarchy, so it doesn't seem to be suitable as a hierarchical operation. Exploring this problem is an important part of considering the introduction of the operator. Please don't use that as a way to discount the concept, because it has no effect on its veracity as an operator (the same problem would occur with intersection).

    I disagree with intersection having any benefit over this operator. It is not an "in between" operator. Unlike intersection, It is a variation of multiple inheritance that was designed specifically for the "merging" of objects. It is backwards compatible with inheritance (and will always be, including with future changes to the lanaguge) and works with hybrid and strict types (which are theoretically incompatible with intersection). It is conceptually simpler and easier to understand than intersection. It allows significant reuse of code in the compiler.

    Your statement was general and subjective and not backed up by well founded arguments. I would like to hear a more detailed and logical explanation (not opinion) of why you think intersection is more desirable than this (or some future adaption of it). I would also need to hear a similar set logical and well-founded set of arguments from Anders Hejlsberg (@ahejlsberg) (again, not opinion).

    If the points that are given would be insightful and helpful. I will try to address them and modify/improve the design. Constructive criticism is always welcome.

  9. rotemdan commented on Aug 26, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman) Anders Hejlsberg (@ahejlsberg)

    I just realized I was mistaken by assuming overloading properties assigned function types is already possible (or even ever would be) through inheritance:

    interface A {
        func(arg: string): boolean;
    }
    
    interface B extends A {
        func(arg: number): string;
    } // Error! Interface 'B' incorrectly extends interface 'A'

    Allowing this would have the same assignability problem as with properties because overloading essentially "widens" the types (both properties and function signatures) so it would break the expected supertype-subtype relation correlation with position in the chain.

    As a non-expert it was not initially obvious for me to notice that..


    Anyway, I've been observing the discussions and suggestions related to intersection on the site and it seems to me almost everyone (except perhaps the designers themselves) misunderstand intersections to some degree, and some seem to propose things that imply stretching or even "abusing" the math at places:

    Typescript Design Goal:

    Produce a language that is composable and easy to reason about.

    Honestly, it looks more like it requires a Ph.d just to get around the theory here, when also considering the inherent complexities of the language itself (try explaining intersection types to high-school students.. good luck with that..).

    Also I think that's unavoidable that people would keep viewing and using intersection as (I wouldn't say "primarily" but that may actually turn out to be the case..) a way to perform "ad-hoc" multiple inheritance. Although now having local types declarable in functions would reduce that, but only by a little:

    function test() {
        interface C extends A,B {}
        let x: C;
    }

    This would still not address the need to have something equivalent for class/interface property types, return types, function arguments, generic types etc. My proposed operation encompasses virtually all the functionality of multiple inheritance and intersection, in one operator. It is not an in-between, it is both, and possibly more (also, this better addresses the design goal: "produce a language that is composable").

    I feel like I've already done all I can (and probably well over and beyond what I should have). I was just trying to do my best to help make the language better (for my own, and everyone's benefit). Without your cooperation, that would be pretty much impossible.

  10. JsonFreeman commented on Aug 26, 2015

    @JsonFreeman
    Contributor

    I am not sure what 'composable' means here.

    The concept of overloading has to do with aggregating signatures. It is not an operation on types, it is an operation on signatures. Overloading means that something is capable of being called or new-ed in multiple ways.

    You are right that inheritance with methods (like your func example) follows the property model of inheritance, and not the signature model. Namely, the method from the derived class completely replaces the base method.

    Rotem Dan (@rotemdan) I think the issue is that you are proposing and discussing so many ideas at the same time. It is easier to deal with questions / issues / confusions / requests one by one, instead of all at once.

  11. rotemdan commented on Aug 26, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)
    I understand what mean about the concept of overloading (and I might have been mistaken in my self-correction here - maybe I just forgot my original line of thought). Anyway, when I used the term "structural overloading" I referred to a generalized form the concept (of aggregating signatures) which can be applied to properties as well through unions. This was done by analogy, to try to capture the closest "sense" of what a useful operator might do. The idea is that the designers can define and work out the operator directly in a way that would work best fit with the language (AKA do the "right thing") without having to "sign-in" to a rigid mathematical scheme that only incidentally seem to be useful, at least in some cases.

    The main intention of this proposal, plain and simple, is to provide an example (or at least a direction) of what could replace intersection. I'm not a world class language expert of the level of yourself and the people you work with. I cannot provide you with detailed specifications of how to achieve and work out the problems that may occur. I'm not familiar with the compiler code and I never took part in design meetings (especially the ones that discussed intersection).

    If I make a mistake that looks fundamental or basic to you, or I mention I'm not sure about something, I guess that would mostly demonstrate that I'm not an expert. It's nice of you to try to help, but that's is not really the point here. Again, I didn't open this issue to understand intersection better. I opened it to encourage people to consider its alternatives.

    If intersection is released to Beta soon, then I guess have failed and there's no point in continuing. After all, this is Andres' feature. If anything I mentioned here (and at other threads) made any impact or even ringed a bell. He could easily come up with a replacement. I don't think any one of you would need my assistance.

  12. JsonFreeman commented on Aug 27, 2015

    @JsonFreeman
    Contributor

    That is fair. I understand that the goal of the proposal is to replace intersection because there are people who find it unintuitive. And I understand that you have thought about several flavors of alternatives based on other models that are used in the language.

    I do not believe intersection is likely to be replaced. There is no concrete evidence of it being limiting to users in terms of expressiveness and utility, only that several users are more familiar and comfortable with other concepts instead. In some cases, that is enough to oust or deprecate a feature, and in some cases it's not. My hunch is that in this case it is not, particularly because the design and interaction with the rest of the type system would have to be worked out again with the alternative feature, and is likely to have challenges that differ from those of intersection. So my hunch is that it will not get replaced, but Anders Hejlsberg (@ahejlsberg) will have an opinion.

  13. rotemdan commented on Aug 27, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    I appreciate your response. I disagree that the word "deprecate" should be used with the feature because as of this time it is not a production feature (or even Beta - though it seems like that would be close). I value the work and thought that has been done to design and integrate it but I thoroughly believe it is not the best fit for the language (and not a great idea in general).

    I have already expressed so many of the issues and concerns I have with it (and again, not all of them were about problems with intuition and understanding, some are about "impedance mismatches" with inheritance, strict/hybrid types etc). Some of that was done when the code was still at the state of a pull request (I only became aware of intersection types when they were added to the roadmap, unfortunately, that might have been a bit too late, because the code was already written).


    There's an interesting idea that I've been considering with this proposal, that I haven't mentioned yet, and is strongly related to the issue of backwards-compatibility and composability (which you asked about):

    Let A and B be any types. Now consider the following:

    If interface C extends A,B {} does not fail, i.e. A and B can be used with standard multiple inheritance. Then A overload B could yield the same semantics, including even preserving the resulting inheritance chain. I mean, the compiler can use a trial-and-error approach and first try normal inheritance, and only if it fails resort to a more complex method (that would probably not preserve the inheritance chain due to assignability issues etc.).

    This will increase composoability because type C = A overload B would be guranteed to always yield exactly the same result as interface C extends A,B {} (unless the second fails). This may even include cases with classes (class C implements (A overload B)).

    At this point (and with my level of expertise) I can't really predict if this would turn out to be a great idea, but I think it demonstrates just how creative one can be when a more open-ended toolset is available.

    [I'm aware of my tendency of "bundling" unrelated ideas and questions together. I am trying to improve on that but in this case don't feel that opening more issues would be a good idea].

    Edit: I had an unintended mistake: by interface C extends (A overload B) {} I meant type C = A overload B. I corrected the text.

  14. rotemdan commented on Aug 30, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    I just realized that I for the specific case of "merging" of objects like is done with jQuery.extend perhaps it would be better to use unions instead of overloading for the case of properties having function types:

    interface A {
        func(number): string;
        func(number): number;
    }
    
    interface B {
        func(string): boolean;
    }
    
    interface FunctionMergingWithOverloading {
        func(number): string;
        func(number): number;
        func(string): boolean;
    }
    
    interface FunctionMergingWithUnions {
        func: { (number): string; (number): number; } | { (string): boolean };
    }

    The version with unions would probably be more correct for assignablilty in that case: either to, or from (with a cast).

    The version with overloading would still be useful though, but for other cases, such as extending an old API to a slightly modified newer one (say, one that adds some overloads to a single function) without needing to completely restate the older one.

    I realize that might seem obvious for experts, but reasoning about types is not something I do daily. Anyway, I didn't consider that a critical detail. I should probably change the title to "Structural merging" though, or maybe even start a new proposal for it.

  15. JsonFreeman commented on Aug 31, 2015

    @JsonFreeman
    Contributor

    Overloading and unions mean different things. A type with overloads (like FunctionMergingWithOverloading in your example) means that the value can be called in any of those three ways, and the caller gets to choose which one to use at any time.

    A union of function types on the other hand means that the value might be of type A or of type B when the caller obtains it, but the caller is not sure which one it got. In cases like that, the caller really can't assume that any signature exists on the type, and needs to inspect the value further to find out if it was really an A or a B.

    In light of this, are you sure jquery.extends gives you a union? Normally when you extend an object with additional functionality, you produce an intersection (which for function types means overloads).

  16. 7 remaining items

  17. rotemdan commented on Sep 2, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    "Conflict resolution" is a general way to describe any automatic method the compiler uses to deal with type conflicts between identically named properties - the two discussed methods were overloading (for properties with function types) and unions. I generalized it to something independent of theoretical or mathematical frameworks.

  18. JsonFreeman commented on Sep 2, 2015

    @JsonFreeman
    Contributor

    Thanks for explaining. But seeing the explanation for ad hoc multiple inheritance makes me come full circle to the idea that intersection types already do what you want. I am once again not understanding what is lacking from intersection types even though it seems like you explained it many times.

  19. rotemdan commented on Sep 2, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    Anyway, now that 1.6 is finalized (I'm not sure if to Alpha, Beta, or Release, though) and intersection as well, all I can say is that it is unfortunate that the most common use case of intersection would now be replicating ad-hoc inheritance.

    It is sad to see people use the verb "intersect" to describe inheritance (AKA "newspeak"). It is sad to see TS team members perpetuate this over and over again. If there is no need to resolve identically named properties with incompatible types (i.e. "conflicts") then the operation would just be equivalent to inheritance (with or without errors on cycles, I don't see that as a huge problem for a structurally typed language). I strongly prefer it to be named that way, because that is exactly what it is.

    Theoretically it also may be a step back with serious implications about subtyping.


    My original proposal titled "Inheritance with overloading" described a hierarchical operator in the spirit of extends. Revisiting it now, it doesn't seem to be problematic if "conflicts" are only resolved for functions through overloading. Super/subtype assignability is still preserved in relation to the position in the chain:

    interface A {
      func(arg: number): boolean;
    }
    
    interface B overloads A {
      func(arg: string): number;
    }
    
    let a: A;
    let b: B;
    a = b; 
    // This assignment is valid since overloading does not have the 
    // problem of "widening" types as would be with unions.
    
    b = a; // Error: incompatible types
    // This assignment would fail because func has two overloads in B and only one in A.

    So it is possible to define a midway hierarchical operation that would overload on function redefinitions, but error on property redefinitions:

    interface A {
      prop: number;
    }
    
    interface B overloads A {
     prop: string;
    } // Error: property "prop" is re-defined. 
    // This would be disallowed to preserve assignability to the super-interface A

    I'm just giving it as another example of the range of alternatives that could have been used (though it would not substitute the entire functionality of intersection, it would be sufficient in many cases).

    [This was written before your response, so I will continue in the next comment]

  20. rotemdan commented on Sep 2, 2015

    @rotemdan
    Author

    Jason Freeman (@JsonFreeman)

    Both Andres and you mentioned the requirement for using the operator with generic type parameters. I would first say that sounds like a constraint, not a design goal, and it is not clear to me why it should dictate major design decisions. I think that in order to understand it, I would need a detailed explanation of what it is and where it comes from. The original statement was like this:

    declare function combine<T, U>(obj1: T, obj2: U): T & U;

    What does this really imply? The most that I could understand so far is that it is not possible to know that types of T and U, check if they are incompatible, and error in advance. Even when using a type constraint? This seems very technical and I haven't got a really good understanding of why this is such a problem?

  21. JsonFreeman commented on Sep 2, 2015

    @JsonFreeman
    Contributor

    Intersecting (or merging, combining, whatever you want to call it) generic type parameters is actually a main use case. It is what people wanted, and is the most generally useful. That way, people can define functions like combine that work on any two types.

  22. JsonFreeman commented on Sep 2, 2015

    @JsonFreeman
    Contributor

    Intersection is not fundamentally about merging properties by the way. It is about simultaneously being two or more types at the same time. If the types are defined by their properties, then this does mean the properties get combined. But not every type is defined by properties (the most obvious example being union types).

  23. added
    SuggestionAn idea for TypeScript
    DeclinedThe issue was declined as something which matches the TypeScript vision
    Out of ScopeThis idea sits outside of the TypeScript language design constraints
    on Sep 17, 2015
  24. mhegazy commented on Sep 17, 2015

    @mhegazy
    Contributor

    The language already has a concept of inheritance, and now intersection types. We would like to limit the complexity in this area in the time being.

  25. rotemdan commented on Sep 18, 2015

    @rotemdan
    Author

    Mohamed Hegazy (@mhegazy)

    This was a discussion about a direct alternative and replacement for intersection types, and was suggested before it was finalized into the language. That is mentioned many times in the discussion.

    Since intersection types have been finalized, I agree it would not be wise to introduce a duplicate feature. I've mentioned that several times as well, including two and a half months ago, even before the code for intersections was committed into the development branch.

  26. wclr commented on Sep 11, 2016

    @wclr

    So it is not possible neither to overload or subtract property from the interface?

    interface A {
      x: number
      a: string
    }
    
    interface B extends A {
      a: undefined // will error Interface 'B' incorrectly extends interface 'A'.
    }
    // want `a` be forced undefined or optional
    let b: B = {x: 1, a: undefined} // will error 
  27. woutervh- commented on Nov 9, 2017

    @woutervh-

    With advanced types you can use the "Omit" pattern to remove fields from an interface/type:

    type Diff<T extends string, U extends string> = ({[P in T]: P} & {[P in U]: never} & {[x: string]: never})[T];
    type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
    type Overwrite<T, U> = Pick<T, keyof Omit<T & U, keyof U>> & U;
    
    interface A {
      x: number;
      y: number
    }
    
    type B = Omit<A, 'x'>; // {y: number}
  28. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionOut of ScopeThis idea sits outside of the TypeScript language design constraintsSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions