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

Proposal: structural overloading for interfaces #4302

Closed
rotemdan opened this issue Aug 13, 2015 · 31 comments
Closed

Proposal: structural overloading for interfaces #4302

rotemdan opened this issue Aug 13, 2015 · 31 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@rotemdan
Copy link

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.

@JsonFreeman
Copy link
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).

@rotemdan
Copy link
Author

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

@rotemdan rotemdan changed the title Proposal: overloading inheritance Proposal: structural overloading for interfaces Aug 14, 2015
@rotemdan
Copy link
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;
}

@rotemdan
Copy link
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).

@rotemdan
Copy link
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.

@JsonFreeman
Copy link
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.

@rotemdan
Copy link
Author

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

@rotemdan
Copy link
Author

@JsonFreeman @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.

@JsonFreeman
Copy link
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.

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

@rotemdan
Copy link
Author

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

@JsonFreeman
Copy link
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 @ahejlsberg will have an opinion.

@rotemdan
Copy link
Author

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

@rotemdan
Copy link
Author

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

@JsonFreeman
Copy link
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).

@rotemdan
Copy link
Author

@JsonFreeman

The difference seems subtle (at least for a non-expert like me). But I realized that to simulate the type equivalent of the merge operation (as applied by jQuery.extend), it would be more correct to use function unions (assuming I'm not mistaken again, anyway, this has been a great learning experience so far .. :) )

When two or more object arguments are supplied to $.extend(), properties from all of the objects are added to the target object. Arguments that are null or undefined are ignored.

[By "arguments" in the last sentence they probably meant "properties"].

So for the case:

interface A {
    prop: number;
    func(arg: string): boolean;
}

interface B {
    prop: string;
    func(arg: number): string;
}
let a: A = { prop: undefined, func: (arg: string) => true }
let b: B = { prop: "hello", func: (arg: number) => "abcd" }

let merge = jQuery.extend(a, b) would equal { prop: "hello", func: (arg: number) => "abcd" }. So b.func would override a.func, not "overload" it (I believe this would even be the case in the "deep" version).

A different case:

let a: A = { prop: 42, func: (arg: string) => true }
let b: B = { prop: "hello", func: undefined }

let merge = jQuery.extend(a, b) would equal { prop: "hello", func: (arg) => true }. b.func is undefined so would be ignored, the resulting value for the func property should equal a.func.

Modeled with overloading, the resulting type would look like this:

interface WithOverloading {
    prop: number | string;
    func: { 
        (arg: string): boolean;
        (arg: number): string; 
    }
}

let a: A;
let b: B;
let merge: WithOverloading;

The problem here is that it is not possible to assign a.func or b.func to merge.func. But it is possible to assign in the other direction without a cast (i.e. b.func = merge.func) which may be a mistake to allow.

The alternative approach with unions would look like this:

interface WithUnions {
    prop: number | string;
    func: { (arg: string): boolean } | { (arg: number): string }; 
}

let a: A;
let b: B;
let merge: WithUnions;

Now it is possible to assign a.func or b.func to merge.func. Assigning in the other direction would require an explicit cast b.func = <(arg: number) => string> merge.func [edit: it is also possible to state that more simply as b.func = <typeof b.func> merge.func].

@rotemdan
Copy link
Author

When two or more object arguments are supplied to $.extend(), properties from all of the objects are added to the target object. Arguments that are null or undefined are ignored.

Re-reading this, maybe they did actually mean "arguments" literally? (so in that sentence they referred to the arguments for the extend function itself?). Perhaps what I had in mind was this (I have my own implementation with a similar logic):

Undefined properties are not copied. However, properties inherited from the object's prototype will be copied over. Properties that are an object constructed via new MyCustomObject(args), or built-in JavaScript types such as Date or RegExp, are not re-constructed and will appear as plain Objects in the resulting object or array.

Source: jQuery.extend() documentation.

@JsonFreeman
Copy link
Contributor

I think the semantics of this jquery.extends operation are very specific. It seems like it chooses which object to copy from, on a per property basis. The closest thing we have in the language to model this is unions, as you point out. But this

{
    prop: number;
    func(arg: string): boolean;
} |
{
    prop: string;
    func(arg: number): string;
}

is not the same as this

{
    prop: number | string;
    func: { (arg: string): boolean } | { (arg: number): string }; 
}

because in the first case, you can't have an object where prop is a number, but func takes a string.

The tricky thing is that if you introduce a type operator to typescript, you have to think about whether it can occur in a syntactic context where a type parameter can appear as an operand. If it can, then you either have to explicitly require that it only be applied to object types (for example the extends operator), or you have to allow it for all types (for example union types). The reason is that you don't know what the type parameter will be instantiated to. I realize this seems like an abstract concern, but it is a very real phenomenon.

@JsonFreeman
Copy link
Contributor

I'll also add that in terms of non-overlapping properties, the merging operation you're describing aggregates them in an intersection-like manner, rather than a union-like manner. This means that the operation you're looking for at times resembles intersection and at times resembles union.

@rotemdan
Copy link
Author

rotemdan commented Sep 1, 2015

@JsonFreeman

I'm aware of the differences between (whole) unions of object types and structural union of their properties. Although I may not have the expert level view to understand its theoretical implications.

The reason I was trying to model jQuery.extend(a, b) was because it is a real world idiom that's very commonly used, and the original design goal of intersection was to model a combine function so I basically tried to follow that.

In my own code I use a similar object merging method but in terms of types the arguments a, b, they are virtually always modeled with a single interface type containing mostly (or all) optional parameters. The return type is almost always the same type as the first argument of the function. The types of the objects that are merged into it are either the same or structural supertypes of it (i.e. contain only a "fragment" of its properties [with consistent types]).

interface Options {
  startIndex?: number;
  endIndex?: number;
  loop?: boolean;
  callback?: (number) => boolean;
}

function SomeAction(userOptions: Options) {
  let defaultOptions: Options = {
    startIndex: 0,
    endIndex: undefined,
    loop: false,
    callback: undefined
  }

  // The merged object and arguments will all have the same types here,
  // although it is possible that the extendObject function would generate an object having
  // a different type that virtually never happens in practice.
  userOptions = extendObject<Options>(userOptions, defaultOptions) 
}

I never felt a real-world need for a specialized "type combination" operator for this scenario. It is actually hard for me to think of cases where the concepts of intersection or structural overloading/merging would be useful for these purposes (I mean, even if they do manage to model it correctly). Especially when it comes to function type conflicts, where not being able to deterministically predict the signature wouldn't be very useful.

Anyway, it's true that in practice extend is limited to object types, but that doesn't mean the type operator can't be made backwards compatible with inheritance so it could work on function types, constructor types, hybrids etc. so also provide ad-hoc multiple inheritance for pure type composition. The issues that are being mentioned here (whether to use overloading or unions for consolidation of properties with function types) are only relevant for cases where inheritance would fail, i.e. fallback mechanisms for "conflict resolution".

The issue with generic type parameters seems like a very technical one, that I personally don't completely understand. And not completely convinced it is a justified reason to determine major design decisions.

So at this point I feel that without clear design goals and convincing use cases, any proposed operator would feel arbitrary to a degree. My original intention here was not to propose an alternative because it is ultra-useful for real-world object merging, but to show an alternative to an existing one that I thought was problematic (on many levels). As I've mentioned several times, I think most of the real-world usefulness here comes from the ability to apply ad-hoc multiple inheritance, not specifically the "conflict-resolution" feature. That's why I felt it was important to maintain a high (or even complete) level of backwards compatibility with it.

@JsonFreeman
Copy link
Contributor

Sorry, I am not following. Specifically, what are "ad hoc multiple inheritance" and "conflict resolution"?

We are also using terms differently. When I say "object types", I am including what you are calling "function types" and "hybrid types".

@rotemdan
Copy link
Author

rotemdan commented Sep 2, 2015

@JsonFreeman

Ad-hoc multiple inheritance means it is possible to replicate the effect of inheritance through an operator:

interface A {
  prop1: number;
}

interface B {
  prop2: string;
}

class Test {
  // The return type of this method is an ad-hoc interface equivalent to
  // interface B extends A { [original content of B] }
  // which would equal { prop1: number, prop2: string }
  func(arg: number): B extend A 
}

I used "object types" to refer to interfaces that contain only properties (and no function or construct signatures)
I used "function types" to refer to interfaces that contain only function signatures.
I used "hybrid types" to refer to interfaces that contain both function signatures and properties.
I used "constructor types" to refer to interfaces that contain construct signatures and possibly properties.

Please ask if there more things I wrote that unclear. As an "outsider" it is not obvious for me to align with your terminology and completely understand the conventions used to discuss the language (of the sort that would happen in design meetings etc.) .

@rotemdan
Copy link
Author

rotemdan commented Sep 2, 2015

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

@JsonFreeman
Copy link
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.

@rotemdan
Copy link
Author

rotemdan commented Sep 2, 2015

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

@rotemdan
Copy link
Author

rotemdan commented Sep 2, 2015

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

@JsonFreeman
Copy link
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.

@JsonFreeman
Copy link
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).

@mhegazy mhegazy added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision Out of Scope This idea sits outside of the TypeScript language design constraints labels Sep 17, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Sep 17, 2015

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.

@mhegazy mhegazy closed this as completed Sep 17, 2015
@rotemdan
Copy link
Author

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

@wclr
Copy link

wclr commented Sep 11, 2016

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 

@woutervh-
Copy link

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}

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Declined The issue was declined as something which matches the TypeScript vision Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants