-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Intersection types #3622
Conversation
@@ -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)) { |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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
Nice! I have not yet looked at the implementation, but I have a few comments about the design summary:
|
@@ -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, |
There was a problem hiding this comment.
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?
@JsonFreeman Some answers:
Correct. In particular, because type parameters can be intersected we have to allow intersections of all kinds of 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.
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
Yup.
Indeed, but only as long as we preserve order of constituent types. |
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.
Yes, I remember thinking that when we came up with contextual typing for union types. The mechanism for union types is rather intersection-y.
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, |
function parseUnionTypeOrHigher(): TypeNode { | ||
let type = parseArrayTypeOrHigher(); | ||
if (token === SyntaxKind.BarToken) { | ||
function parseUnionOrIntersectionType(kind: SyntaxKind, parseConstituentType: () => TypeNode, operator: SyntaxKind): TypeNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice reuse!
@ahejlsberg 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. |
If we begin with |
@tinganho See if the example at #3622 (comment) and my reply #3622 (comment) clarify why combining the properties gives you the intersection. |
@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. |
@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"? |
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 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):
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. |
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! |
@wgebczyk 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:
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 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). |
Off Topic & Rants: 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 :) |
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... |
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 |
What's the observable difference between the types
and
? 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. |
@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. |
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. |
Thoughts on changing
to
So the last declared type wins, see #4805 That gives an explicit way to resolve conflicts:
|
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 typeA & B
represents an entity that has both type A and type B.Type Relationships
A & A
is equivalent toA
.A & B
is equivalent toB & A
(except for call and construct signatures as noted below).(A & B) & C
is equivalent toA & (B & C)
.A & B
is equivalent toA
ifB
is a supertype ofA
.Assignment compatibility
A & B
is assignable toX
ifA
is assignable toX
orB
is assignable toX
.X
is assignable toA & B
ifX
is assignable toA
andX
is assignable toB
.Properties and Index Signatures
The type
A & B
has a propertyP
ifA
has a propertyP
orB
has a propertyP
. IfA
has a propertyP
of typeX
andB
has a propertyP
of typeY
, thenA & B
has a propertyP
of typeX & Y
.Index signatures are similarly intersected.
Call and Construct Signatures
If
A
has a signatureF
andB
has a signatureG
, thenA & B
has signaturesF
andG
in that order (the order of signatures matter for purposes of overload resolution). Except for the order of signatures, the typesA & B
andB & A
are equivalent.Precedence
Similarly to expression operators, the
&
type operator has higher precedence than the|
type operator. ThusA & 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 thanundefined
). 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
Fixes #1256.