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

Intersection types #3622

Merged
merged 14 commits into from
Jul 6, 2015
Merged

Intersection types #3622

merged 14 commits into from
Jul 6, 2015

Conversation

ahejlsberg
Copy link
Member

This PR implements intersection types, the logical complement of union types. A union type A | B represents an entity that has either type A or type B, whereas an intersection type A & B represents an entity that has both type A and type B.

Type Relationships

  • Identity: A & A is equivalent to A.
  • Commutativity: A & B is equivalent to B & A (except for call and construct signatures as noted below).
  • Associativity: (A & B) & C is equivalent to A & (B & C).
  • Supertype collapsing: A & B is equivalent to A if B is a supertype of A.

Assignment compatibility

  • A & B is assignable to X if A is assignable to X or B is assignable to X.
  • X is assignable to A & B if X is assignable to A and X is assignable to B.

Properties and Index Signatures

The type A & B has a property P if A has a property P or B has a property P. If A has a property P of type X and B has a property P of type Y, then A & B has a property P of type X & Y.

Index signatures are similarly intersected.

Call and Construct Signatures

If A has a signature F and B has a signature G, then A & B has signatures F and G in that order (the order of signatures matter for purposes of overload resolution). Except for the order of signatures, the types A & B and B & A are equivalent.

Precedence

Similarly to expression operators, the & type operator has higher precedence than the | type operator. Thus A & B | C & D is parsed as (A & B) | (C & D).

Primitive Types

It is possible to intersect primitive types (e.g. string & number), but it is not possible to actually create values of such types (other than undefined). Because such types can result from instantiation of generic types (which is performed lazily), it is not possible to consistently detect and error on the operations that create the types.

Examples

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result;
}

var x = extend({ a: "hello" }, { b: 42 });
var s = x.a;
var n = x.b;
type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
interface A { a: string }
interface B { b: string }
interface C { c: string }

var abc: A & B & C;
abc.a = "hello";
abc.b = "hello";
abc.c = "hello";

interface X { x: A }
interface Y { x: B }
interface Z { x: C }

var xyz: X & Y & Z;
xyz.x.a = "hello";
xyz.x.b = "hello";
xyz.x.c = "hello";

type F1 = (x: string) => string;
type F2 = (x: number) => number;

var f: F1 & F2;
var s = f("hello");
var n = f(42);

Fixes #1256.

@@ -1538,8 +1539,8 @@ namespace ts {
else if (type.flags & TypeFlags.Tuple) {
writeTupleType(<TupleType>type);
}
else if (type.flags & TypeFlags.Union) {
writeUnionType(<UnionType>type, flags);
else if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a UnionOrIntersection

result.push(unionProp);
function getPropertiesOfUnionOrIntersectionType(type: UnionOrIntersectionType): Symbol[] {
for (let current of type.types) {
for (let prop of getPropertiesOfType(current)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: property instead of prop

@JsonFreeman
Copy link
Contributor

Nice!

I have not yet looked at the implementation, but I have a few comments about the design summary:

  • You mention that primitives can be intersected. I assume any kind of type can be intersected, including union types, right?
  • How does type argument inference work with intersection types? It is similar to union types?
  • How does contextual typing work with intersection types? It is similar to union types?
  • When you say "A & B is assignable to A and assignable to B", seems like it is more generally correct to say "A & B is assignable to X if A is assignable to X or B is assignable to X. When X is A or B, this falls out from reflexivity.
  • The call and construct signatures work out much nicer here than they do for union types.

@@ -1639,6 +1645,8 @@ namespace ts {
StringLike = String | StringLiteral,
NumberLike = Number | Enum,
ObjectType = Class | Interface | Reference | Tuple | Anonymous,
UnionOrIntersection = Union | Intersection,
StructuredType = ObjectType | Union | Intersection,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining why these are considered structured?

@ahejlsberg
Copy link
Member Author

@JsonFreeman Some answers:

You mention that primitives can be intersected. I assume any kind of type can be intersected, including union types, right?

Correct. In particular, because type parameters can be intersected we have to allow intersections of all kinds of types.

How does type argument inference work with intersection types? It is similar to union types?

It is similar to union types on the source side: If the source type is a union or intersection type, we infer from each of the constituent types. We currently make no inferences if the target is an intersection type, but I'm thinking we should probably infer to each constituent type that isn't a type parameter, similar to what we do for a union type. However, the secondary inference we make to a single naked type parameter in a union type doesn't seem to make sense for an intersection type.

How does contextual typing work with intersection types? It is similar to union types?

We do nothing special when an intersection type is a contextual type, i.e. it appears to have the same properties with the same types as in an expression. Actually, it's interesting that when a union type A | B | C is used as a contextual type we treat it the same as A & B & C.

When you say "A & B is assignable to A and assignable to B", seems like it is more generally correct to say "A & B is assignable to X if A is assignable to X or B is assignable to X. When X is A or B, this falls out from reflexivity.

Yup.

The call and construct signatures work out much nicer here than they do for union types.

Indeed, but only as long as we preserve order of constituent types.

@JsonFreeman
Copy link
Contributor

However, the secondary inference we make to a single naked type parameter in a union type doesn't seem to make sense for an intersection type.

I agree, because in the union case, you are saying that maybe the target is a type parameter, and if it is, the whole source type should be inferred to this type parameter. For intersection, the target type is definitely a bunch of things, including a type parameter, but you don't really know "how much of the source type" should be inferred to the type parameter portion. I guess intersection has this element of partiality, where union types are more all-or-none.

Actually, it's interesting that when a union type A | B | C is used as a contextual type we treat it the same as A & B & C.

Yes, I remember thinking that when we came up with contextual typing for union types. The mechanism for union types is rather intersection-y.

Indeed, but only as long as we preserve order of constituent types.

Actually, we have a similar commutativity hole in union types, but it has to do with subtype reduction. It's similar to #1953:

interface A {
    (): string;
}
interface B {
    (x?): string;
}

Ideally, A | B should have a call signature with no parameters, but B | A should have a call signature with an optional parameter. This is different from the intersection case because it has to do with subtype reduction, whereas the commutativity hole for intersection does not. But we should probably address this commutativity hole with union types (possibly by changing subtype for optional parameters).

function parseUnionTypeOrHigher(): TypeNode {
let type = parseArrayTypeOrHigher();
if (token === SyntaxKind.BarToken) {
function parseUnionOrIntersectionType(kind: SyntaxKind, parseConstituentType: () => TypeNode, operator: SyntaxKind): TypeNode {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice reuse!

@rotemdan
Copy link

rotemdan commented Aug 9, 2015

@ahejlsberg
I find it curious that after years of working with and designing languages like J++ and C# (that's personally one of my favorite languages), which all have strict object and function type semantics, in TypeScript you've decided to fundamentally constrain the language by introducing an operator that is only useful for non-strict types (where a simpler and conceptually easier-to-understand alternative, perhaps like the one I suggested that's based on extension/inheritence-without-error-on-cycles, could be used instead).

Sure, there are partial workarounds like strict object and function literal assignments (which seem reasonable at first sight, at least for most cases, but I haven't really looked into them deeply or used them in practice, I just hope it wouldn't turn out to be a hack that would break in some situations), but that has to be one of the strangest decisions I've seen (well, Javascript and C++ had some horrible language features and design decisions, but you had no part in them, so my expectations are much lower! :) ).

I have spent literally countless amount of hours carefully trying to explain and lay out the complex reasoning why introducing this particular operator in a usage context that strongly suggests it to be used for type extension (which is closer to inheritance, or - simply the union of type specifiers) is fundamentally a bad idea (all is contained in this thread). I have asked many questions and presented numerous very carefully crafted and thought-out arguments about this exact issue and they all went unanswered, literally ignored.

@tinganho
Copy link
Contributor

tinganho commented Aug 9, 2015

When reasoning about JavaScript objects, the full set of possible values for the type {} consists of objects with any set of properties with any spelling and any value (obviously a very large set).

If we begin with {} which is endlessly big. And it has the values { a: any }, { b: any } and { a: any, b: any } amongst the endless values. I still don't agree that { a: any, b: any } represents an intersection of { a: any } and { b: any }. It's like an intersection of [1] and [2] becomes [12] when it should in fact be [].

@JsonFreeman
Copy link
Contributor

@tinganho See if the example at #3622 (comment) and my reply #3622 (comment) clarify why combining the properties gives you the intersection.

@tinganho
Copy link
Contributor

tinganho commented Aug 9, 2015

@JsonFreeman After reading it a couple of times. I think I understand your reasoning. All objects are extensible and when doing an intersection it will intersect between the possible extension values. Though I must admit that it wasn't so straightforward.

I think most people think of TS interfaces as not extensible if you don't extend it. You can't for instance not do:

interface A {
    a: boolean
}
let a: A;
a.b = true; // error

So the naming of intersection is still a little bit confusing for me.

@wgebczyk
Copy link

wgebczyk commented Aug 9, 2015

@JsonFreeman "And this is an intersection because by virtue of having both properties a and b , an object satisfies both constraints of having property a and having property b".

What about that sentence: "And this is an intersection because by having both elements a and b, an set satisfies both constraints of having element a and having element b".

It seems that in your worlds of language composition, system types, compilers and parsers, intersection word has different meaning that for regular developer (like me) that was designing only every simple DSL languages in area of "language design". I accept fact that in your domain you have different languages that might share same written words.

The question is, do you want ot leak internal "language composition" term that collides with the same work for costumers of TS. On the other hand if you want to keep "intersection" only in design documents and do not advertise this language feature to consumers under that name, I'm completely fine!

I as regular developer that comes from typed languages, had a few times problem to understand advertised features (let me mention, guard expressions or type aliases...). I just want to tell you that some things for you might be obvious, are not so obvious to group of people that seems to be target group of TS consumers nada I'm sure that this group is not small and at same time its not majority. Go talk to some UX person on technical and perceived point of views.

Or maybe my assumption that I belong to some group that has never been envisioned to be "target group"?

@rotemdan
Copy link

rotemdan commented Aug 9, 2015

@wgebczyk

The confusion is not just related to terminology, but also with the implicit assumption taken by @ahejlsberg and @JsonFreeman that the underlying types are non-strict, which may be different from the intuition and possibly the expectations held by most programmers used to typed languages (myself included, at least initially). This is a conceptual, not a naming issue, that has to do with the scope on which intersections are defined and useful on.

The intersection of two strict (i.e. "closed" or precisely defined) object interfaces A and B would always yield the empty set with the exception being where Val(A) is a subset of Val(B) or Val(B) is a subset of Val(A) (where Val(X) is the set of values satisfying type X). [Edit: this assertion ignores optional properties, for simplicity. Also, strict function interfaces would respond a bit differently to intersection]

Most programmers coming from static (or even more generally, typed) languages tend to see most types as strict, possibly creating a (justified) sense of confusion and uncertainty when confronted with the "intersection" terminology (and concept):

  • Classes - strict in C#, Java, C++ and in TypeScript as well, though intersection is not defined on them and there is no current way to extract interfaces from them.
  • Structs - strict in C#, C++ and C (don't exist in Java). Implemented with non-strict interfaces in TypeScript (or possibly with classes, but that's a less useful approach because classes do not support optional properties or literal assignments).
  • Delegate types (function signature types) - strict in C# (don't exist in C++, not sure about Java). Implemented with non-strict function interfaces in TypeScript.
  • Interfaces - in C# and Java they are not purely strict but still nominal (relation with a supertype needs to defined explicitly, rather than being implicitly inferred from the structure), so they are somewhat non-strict, but in a much weaker way. Non-strict in TypeScript.

The new strict object literal assignment behavior in 1.6 (which is a breaking change) will reduce the need to define purely strict types to some degree, but would still not eliminate it completely. Regardless of this new feature (or any one following it), the introduction of an operator that behaves very poorly with strict types is a strong limitation that would permanently reduce the potential of the language and type system to evolve and develop with time.

@wgebczyk
Copy link

wgebczyk commented Aug 9, 2015

Hmm.. Maybe I overreact. I'll stop giving feedback on new features and only report serious issues/bugs. I'll ignore all features that makes no sense for me and use in way that satisfy my needs. In worst case when TS will drift too much from my expectations, I'll switch to ES6 with some static analysis tools.

I'm sure there are people that would find TS concepts more appealing :)

Thanks for your time for trying to explain me TS!

@rotemdan
Copy link

@wgebczyk
I understand your frustration with the complexity and abstractness of all this. I too feel that it is vastly unnecessary, and that's exactly the reason why I wasted so much of my (unrewarded and unpaid) time thinking about this feature.

I proposed a conceptually simpler alternative based on extension (the "extension" operator) that would instead rely on existing logic and facilities in the language for inheritance and may even share its codebase. An extension operator would be more in-line with the intention of the developer for common use cases and Javascript idioms, and much simpler in concept:

Take interface A. Take interface B. Extend A with B. Done. 

Simple, intuitive and works almost exactly like the familiar, and well understood inheritence operation (though it would need to be more permissive with regards to cycles - probably ignore them to allow things like A extend A - the actual operator could be a symbol, not necessarily a whole word). This could be applied both with object and function interfaces. No issues with strictness whatsoever - it has nothing to do with it (though there would need to be some default logic to determine the strictness of a resulting type, based on the strictness of the operands).

As far as I can tell, my proposal has been rejected without explanation. I was advised to post it as a "feature request" but there is no point - If intersection is finalized into the language then there is no room for another operator that behaves almost exactly the same in most cases (at least until strict types are involved).

@wgebczyk
Copy link

Off Topic & Rants:
@rotemdan I have impression that Mr. @ahejlsberg has drifted too much from initial concept of "introduction of type safety/strict typing". I can understand that after years of creating really good languages, he needed to refresh himself and we got what we have - too far from typed languages too close to weirdness of JS BUT advertised as typed language on top of JS. I can understand that diving into JS so poorly designed, redesigned, with ad hoc changes was challenging, but I think it would be clearly stated that it has less common with C/C++/C#/Java and more with some new concept.
It was my mistake to form some expectations that were never explicitly written: "the guy from rock solid C# (but not perfect) that is pleasure to use will take this crap Wild Wild West JS and set new law. I was using Turbo Pascal then Delphi and now I'm using C#, so new JS called TS will be better world to play with web-thing". Unfortunately AH experiment was based on completely different assumptions. There are really nice features of TS comparable to C# at same time allows to use a few nice features of JS.
They introduced for example union types AND guard expressions AND advertised is really closely together in examples and blog entries. It looked as [aaa|bbb|ccc] and differenating them by [if typeof ...] was only mirage, because it worked only of JS friendly "types" (string, number function, etc) not elements you are "|" - finally it was rather niche feature that a lof of people have problem to use until they realize its big limitations.
They introduced type aliases and i thought as kind of "#define" or "typedef", but again it had some limitations and I falled back to use "import" that is more "define local copy of type(?)" that simple alias.
I could rant on other features like lack of globbing (which hurts a lot when you have a few projects with shared folders and 50k loc & 200k gen loc), delaying release to align with VS, etc.

But still I accept the fact that there is a lot of people that will like and catch on the fly the TS design, its just not for me :)

@kitsonk
Copy link
Contributor

kitsonk commented Aug 10, 2015

I am sure subjective personal attacks on the overall direction have a better place than a merged commit. TypeScript being a fully open source project does have a very simple solution if your "morals" have been offended beyond repair...

@sophiajt
Copy link
Contributor

Hi all - can we keep the conversation focused on the technical aspects rather than opinion pieces or attacks on people?

I'm all for exploring designs and their technical trade-offs. This is goodness.

But please hold off from attacking people.

Likewise, TypeScript has a particular design philosophy. It intentionally doesn't try to match other languages/systems, instead opting to type common JS patterns. You can see more info on the design philosophy on the wiki (notably note #1 of the Non-goals): https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

@LukaHorvat
Copy link

What's the observable difference between the types

{
    f1: int
    f2: string
}

and

{
    f1: int
} &
{
    f2: string
}

?

If they're functionally the same, could the latter one just be reduced to the former one? I've found the feature to be of great use for sort of extensible records but my types get needlessly big when I append to them.

@ahejlsberg
Copy link
Member Author

@LukaHorvat There shouldn't be any semantic differences between the two. However, from a compiler performance point of view, an intersection type made up of many small object types definitely adds more computational load than a single large object type. And, as you point out, the intersection gets messier to look at in error messages and hints. The compiler doesn't attempt to reduce intersection types because it involves analysis and detection of circularly dependent types and can't be done fully when generics and type parameters are involved. We could potentially do something in simple cases, but I'm not sure the effort is worth it.

@LukaHorvat
Copy link

I don't know how common my use case is but it might be the majority. My records are simple and fully typed and I add to them in various steps in my processing pipeline. Super convenient.
I hope this reduction does get implemented in the end.

@jbondc
Copy link
Contributor

jbondc commented Sep 15, 2015

Thoughts on changing

If A has a property P of type X and B has a property P of type Y,
 then A & B has a property P of type X & Y.

to

If A has a property P of type X and B has a property P of type Y,
 then A & B has a property P of type Y.
If A has a property P of type X and B has a property P of type Y, 
then B & A has a property P of type X.

So the last declared type wins, see #4805

That gives an explicit way to resolve conflicts:

type oNumber = { a: number, num: number }
type oString = { a: string, str: string }
type oBoolean = { a: boolean, bool: boolean }
type merge = oNumber & oString & oBoolean; // { a: boolean, num: number, str: string, bool: boolean }

type resolve = merge & {a: number | string | boolean } // { a: number | string | boolean, num: number, str: string, bool: boolean }

Right now this looks super weird:
snap

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.