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

Support intersection types (&)? #9

Closed
ericelliott opened this issue Oct 8, 2015 · 37 comments
Closed

Support intersection types (&)? #9

ericelliott opened this issue Oct 8, 2015 · 37 comments

Comments

@ericelliott
Copy link
Owner

How does the TS intersection work? For instance, how would you model the deep property merge from lodash/object/merge? Do the intersection types allow the flexibility required? The topic of intersection types my need it's own discussion thread to get right, and it may need to diverge from the TypeScript implementation -- or perhaps it's better to advise people to use generic types instead of attempting a strong model for intersections.

@cybrown
Copy link

cybrown commented Oct 9, 2015

Intersection type may be usefull for mixin like behaviours, but I do not know enough typescript and Object.assign (or shims in other libraries) to say if it is strictly the same.

I think it might be obvious when objects have different attribute names (simply merging the attributes with their types in the result), but when they collide, the behaviour may indeed diverge: maybe keeping the attributes of the last type, or merging them recursively.

@tomek-he-him
Copy link
Collaborator

How would we cover the following use case?

We have a generic type with lots of optional fields:

interface Theme {
  name: String,
  background?: Color,
  foreground?: Color,
  border?: Color,
}

We have many concrete uses for the generic type with no more optional fields. But we want to be specific that we can use them wherever a Theme is expected:

interface ColoredBackground {
  name: String,
  background: Color,
}

Is this the rtype way of saying it?

interface ColoredBackground {
  background: Color,
  ...Theme,
}

@tomek-he-him
Copy link
Collaborator

Or do we need new syntax here? & is one option. Another is to reuse the ; from the predicate; interface syntax.

interface ColoredBackground Theme; {
  background: Color,
}

I prefer the current spread syntax.

@ericelliott
Copy link
Owner Author

Maybe I titled this thread wrong. Maybe it should have been "compound types", instead?

If you want ColoredBackground to be a subset of Theme, right? If that's the case, does it need to make any reference to Theme at all?

Seems to me any ColoredBackground would automatically be recognized as a valid Theme because of structural typing.

Is there a real use case for intersection types?

I can see a use case for merges...

@tomek-he-him
Copy link
Collaborator

does it need to make any reference to Theme at all?

Technically no. It’s just valuable info for other users. They can have a bank of Themes and put them wherever a subset of Theme is needed.

@tomek-he-him
Copy link
Collaborator

I can see a use case for merges...

Doesn’t the spread syntax cover them already?

@ericelliott
Copy link
Owner Author

Doesn’t the spread syntax cover them already?

It does... maybe it does so adequately enough.

Still not sure we need this feature at all...

@tomek-he-him
Copy link
Collaborator

Still not sure we need this feature at all...

You mean the spread syntax? I really like how idiomatic and powerful it is. Looks like it covers most use cases for intersection types.

The only thing it doesn’t cover are function intersections (http://flowtype.org/docs/union-intersection-types.html#intersection-example). But this can be expressed differently – in the function signature itself:

// Flow
declare var f: ((x: number) => void) & ((x: string) => void);

// rtype
f(x: Number | x: String) => Void

@ericelliott
Copy link
Owner Author

You mean the spread syntax?

No, I mean &. =)

f(x: Number | x: String) => Void

do you mean this?:

f(x: Number | String) => Void

@tomek-he-him
Copy link
Collaborator

do you mean this?

Yup, good catch!

No, I mean &. =)

We don’t need that if we have the spread, if you ask me. I’ll try to put together a PR which makes it explicit that the spread syntax basically covers what others call “intersection types”.

@ericelliott
Copy link
Owner Author

cool. =)

@tomek-he-him
Copy link
Collaborator

I’ve just had a go at this – and I have one more question.

interface User {
  name: String,
  avatarUrl?: Url,
  about?: String,
  ...properties? // type Object is inferred
}

– what is properties then? Is it just a name of an Object?

Doesn’t it make more sense to spread out a type directly?

interface Person {
  name: String,
  birthYear?: Number,
}

interface AccountHolder {
  id: Number,
}

interface User {
  ...Person,
  ...AccountHolder,
  avatarUrl?: Url,
  about?: String,
}

@ericelliott
Copy link
Owner Author

Doesn’t it make more sense to spread out a type directly?

Yes, it does. Make it happen!

@tomek-he-him
Copy link
Collaborator

Yay! Great news!

First thing on my list for 2016, week 1! I hope it’s OK for you to wait.

@ericelliott
Copy link
Owner Author

Awesome!

@tomek-he-him
Copy link
Collaborator

@ericelliott We have an aweful lot of work this week! Is it OK if it waits a couple of days more?

@ericelliott
Copy link
Owner Author

Sure. =)

@maiermic
Copy link

How does the TS intersection work?

As far as I know TS has no intersection types. But Ceylon has intersection types. Maybe we can learn something from Ceylon. As a side note, I'm new to Ceylon yet.

Are there any other languages or type systems with intersection types we can learn from?

@Mouvedia
Copy link
Collaborator

Flow has it.

http://kwangyulseo.com/2015/06/09/thoughts-on-intersection-types/

So far intersection types seem redundant once a language have union types (or vice versa). However, there are some real world use cases where intersection type are actually required.

@maiermic
Copy link

@Mouvedia 👍

Or do we need new syntax here? & is one option. Another is to reuse the ; from the predicate; interface syntax.

How can I rewrite this code with the existing syntax and without creating a named type definition (I would call I2 & I3 an anonymous type expression)?

interface I1 {
  prop: I2 & I3,
}

How differs spread from &?

@ericelliott
Copy link
Owner Author

How can I rewrite this code with the existing syntax and without creating a named type definition (I would call I2 & I3 an anonymous type expression)?

Perhaps by allowing the ... operator in that context? Note the absence of a comma separator:

interface I1 {
  prop: ...I2 ...I3
}

If you included a comma separator, it would be interpreted as this:

interface I1 {
  prop: ...I2,
  ...I3
}

Which means that I1 (as opposed to prop) includes the properties of I3.

You could also do this:

interface Prop: ...I2 ...I3

interface I1 {
  prop: Prop
}

@BerkeleyTrue
Copy link

How about:

interface I1 {
  prop: { ...I2, ...I3 }
}

@ericelliott
Copy link
Owner Author

I like that a lot. 👍

We should definitely provide an explicit example of that approach. =)

@hax
Copy link

hax commented Jan 13, 2016

Spread operator ... in JavaScript only copy the own properties (See https://github.com/sebmarkbage/ecmascript-rest-spread/blob/master/Issues.md) , So I am not sure use ... to indicate intersection semantic is a good idea.

@ericelliott
Copy link
Owner Author

@hax Why not spread in the relevant delegate prototypes as well, if that's important to your interface?

// User proto
interface Person {
  name: String,
  birthday: Date
}

interface User {
  ...Person, // in JS, this may delegate to Person
  acl: Object
}

interface Admin {
  ...User, // in JS, this may delegate to User
  admin: true
}

@maiermic
Copy link

How differs spread from &?

Does the order of spread matter? Is ...I2 ...I3 the same as ...I3 ...I2?

@ericelliott
Copy link
Owner Author

Does the order of spread matter? Is ...I2 ...I3 the same as ...I3 ...I2?

Good question. In ES6, it was decided that traversal order should be preserved. This could mean that we could apply traversal order to type checking. I think that in 99.9% of cases, traversal order doesn't matter for interface contracts. For the .1% of remaining cases, maybe we could have a keyword to opt into traversal order strictness?

@maiermic
Copy link

If I understand correctly, in ...I2 ...I3 the properties of I3 override properties of I2. If that is correct, spread and intersection are two different things:

interface I2 {
  x: Number
}
interface I3 {
  x: String
}

...I2 ...I3 // x: String
...I3 ...I2 // x: Number
I2 & I3 // Error: incompatible types: x: Number & String

@ericelliott
Copy link
Owner Author

If I understand correctly, in ...I2 ...I3 the properties of I3 override properties of I2

That's true in JavaScript, and you raise a good point.

Clearly, the concerns of a interface description DSL are different from the concerns of JS. Here we have a bit of flexibility. How should it behave, ideally, if you try to mix interfaces with incompatible collisions?

I think an error is probably a wise choice.

@tomek-he-him
Copy link
Collaborator

Lots of 👍s to

interface I1 {
  prop: { ...I2, ...I3 }
}

and

interface I4 {
  ...I2,
  ...I3,
}

As promised, I’ll try to sum this up in a PR – I suggest we move this discussion there as soon as it’s ready. It’s simpler to talk over concrete specs.

@maiermic
Copy link

The only thing [spread syntax] doesn’t cover are function intersections (http://flowtype.org/docs/union-intersection-types.html#intersection-example). But this can be expressed differently – in the function signature itself:

// Flow
declare var f: ((x: number) => void) & ((x: string) => void);

// rtype
f(x: Number | x: String) => Void

Union types can not always be used to express an equivalent type:

moveTo: ((x: number, y: number) => void) & ((p : Point) => void)

is not equivalent to

moveTo: (x: number | Point, y?: number) => void

since you can call the last one with illegal parameters

moveTo(p, y) // with p: Point, y: number
moveTo(x)    // with x: number

How should we treat method conflicts/intersection?

interface I2 {
  f: (x: number) => void
}
interface I3 {
  f: (x: string) => void
}

{ ...I2, ...I3 } might result in an error or could be equivalent to

{ f: (x: Number | String) => Void }

An error would be consequent since spread does not describe intersection.

Conclusion: Intersection and spread syntax should be considered as two different concepts with their own strengths and weaknesses. Intersection is more accurate in regard to overloaded function types and behaviour of conflicting properties (only valid if types of conflicting properties are intersectable).

@ericelliott
Copy link
Owner Author

Is that flow example attempting to communicate polymorphism? If so, we already support that with polymorphic interfaces:

interface F {
  ((x: number) => void),
  ((x: string) => void)
}

How should we treat method conflicts/intersection?

In my opinion, any incompatible type conflicts should be rtype type conflict errors, reportable at compile time.

@maiermic
Copy link

Is that flow example attempting to communicate polymorphism? If so, we already support that with polymorphic interfaces

Yes, I forgot rtype's polymorphic interface notation 😊. Can we use them to express intersection? Is

moveTo: ((x: number, y: number) => void) & ((p : Point) => void)

equivalent to

interface moveTo {
  ((x: number, y: number) => void),
  ((p : Point) => void)
}

for example?

By the way, are parentheses required around a function definition or is it valid to write my example without this clutter

interface moveTo {
  (x: number, y: number) => void,
  (p : Point) => void
}

@ericelliott
Copy link
Owner Author

Parens are not required. This is valid:

interface F {
  (x: number) => void,
  (x: string) => void
}

@ericelliott
Copy link
Owner Author

I'm not sure what you mean by "intersection" in this context. This:

interface moveTo {
  (x: Number, y: Number) => Void,
  (p : Point) => Void
}

Means:

  • "If the first argument is a Number and the second argument is a Number, moveTo will use them as x and y parameters, and will return Void.
  • "If the first argument is a Point, moveTo will use it as parameter p and return Void.

@maiermic
Copy link

Can we use [polymorphic interfaces] to express intersection?

In other words: Can we rewrite "intersection" like in flow with existing rtype syntax?

I just continued on this idea that function intersection can be expressed differently.

@ericelliott
Copy link
Owner Author

@maiermic In order to answer that question properly, what is the difference between "intersection" and a polymorphic signature?

As far as I can tell, there is none. Intersection in flow is either:

A union type:

moveTo(point: Point | EnhancedPoint) => Void

Where Point and EnhancedPoint both satisfy moveTo's requirements for point.

OR, polymorphism:

interface moveTo {
  (x: Number, y: Number) => Void,
  (p : Point) => Void
}

Where x and p represent different things to moveTo, and the function execution is adjusted accordingly. This is also known as ad-hoc polymorphism.

As far as I can tell, rtype already has richer support to break this ambiguity.

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

No branches or pull requests

7 participants