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: remove union types and allow interfaces to have no fields #236

Closed
calebmer opened this issue Nov 1, 2016 · 10 comments
Closed

Comments

@calebmer
Copy link

calebmer commented Nov 1, 2016

I’m only semi-serious with this proposal, and I’m more interested to hear a defense of union types (because I currently can’t think of one) then to see this proposal proceed.


In the specification, it is currently written that “An Interface type must define one or more fields.”. From a precursory glance at the specification it seems like that limitation is arbitrary. In addition, if we remove that validation requirement (and perhaps change some other spec wordings) an interface could behave the exact same as a union, and perhaps be strictly better. An interface with no fields would be better than a union because the interface gives the schema designer the flexibility to add common fields in the future. Because interfaces are nominal and not structural this could be allowed.

A major benefit of unions in functional programming languages is that pattern matching statements with unions can be exhaustive. Whenever a programmer adds a new variant to the union, the type system breaks and requires all exhaustive pattern matches to be update appropriately. In GraphQL such a benefit is not realistic given that the code (mobile apps) which executes against a GraphQL schema can not always easily be updated when adding a new variant to a union. It would seem that the spec makes the argument that unions are good because:

[Unions] also differ from interfaces in that Object types declare what interfaces they implement, but are not aware of what unions contain them. [(source)](They also differ from interfaces in that Object types declare what interfaces they implement, but are not aware of what unions contain them.)

However, it seems to me that the only case where the property of objects not declaring union membership is useful is in a pattern matching scenario which I think we can agree is a scenario that doesn’t always work well with the design goals of GraphQL.

Furthermore, if we make this change removing unions from the specification and allowing interface types with no fields, it wouldn’t be a breaking change. The access patterns from a GraphQL client that assumes unions exist would be the same for an interface with no fields.

So I think it’s plausible that we can remove unions from the spec and maintain backwards compatibility (I’d love to be proven wrong! That’s what this issue is for 😉), and I also think it would have the following benefits:


Again, I’m only semi-serious about this. I mostly just want to hear when you would ever need a union type. I also want to hear future plans for how the union type will evolve because in its current form I don’t see much utility (asides from interfaces with no shared fields).

At a minimum can we remove the requirement for a single field on interfaces? At that point I can remove union types from my projects altogether 😉

@leebyron
Copy link
Collaborator

leebyron commented Nov 2, 2016

Unfortunately this would be a breaking change to the spec for a couple reasons - but I'm not convinced that Unions are not useful simply because they're harder to use in pattern matching scenarios. I also disagree with that, I've seen pattern matching at play with GraphQL Unions to great effect, in fact it's one of a few reasons why Unions in GraphQL are powerful.


First Let's look at client's pattern matching on return types for a Union:

union OneOf = A | B | C

Then in my client code (pseudo code):

switch (oneOf) {
  case A:
  case B:
  case C:
} 

One of two things can happen: First, a type could be removed. Perhaps B gets removed from the union. Clients are naturally resilient to this - they assume B could theoretically appear at some point, even if it never does. Second, and more importantly, a type could be added to the union. Perhaps D gets added. Client's need to be resilient to this as well, so clients that support this kind of exhaustive pattern matching always add one more case:

switch (oneOf) {
  case A:
  case B:
  case C:
  case Unknown:
} 

Because of how unions must be queried, the unknown type is the empty object type.


Next let's look at the ergonomics of defining Unions and Interfaces and the rationale for the restrictions in place.

As you quoted from the spec, Unions are defined locally while Interfaces are defined by implementation of each Object type. This models the data domain in different ways.

A Union is a way to say "I may return one of a few things at this location" - it should be defined locally to the field. For example, a Video type doesn't need to describe that it might appear alongside Photo for some specific fields, though a specific fields needs to describe that it might return Video | Photo.

An Interface is a way to say "I conform to this contract" - it should be defined locally to the thing which conforms, the Object type. For example, a User type should be able to say that it fulfills the contract of Profile such that any type current known or unknown can be used knowing that it fulfills the contract.

This also drives the requirement that Interfaces must define at least one field - otherwise the Interface would not actually describe a contract to be fulfilled. This concept of contract-less interfaces is often called "marker interfaces" and in my opinion (hopefully not an uncommon opinion) are a bad practice borrowed from limited object-oriented languages. They're typically used either as a backfill for languages like Java that don't have a good concept of Union, or are used as metadata annotations for languages that don't have metadata annotations. I don't think either should apply to GraphQL for modeling data domains.

@calebmer
Copy link
Author

calebmer commented Nov 3, 2016

First Let's look at client's pattern matching on return types for a Union, in my client code (pseudo code):

switch (oneOf) {
  case A:
  case B:
  case C:
  case Unknown:
} 

Because of how unions must be queried, the unknown type is the empty object type.

If you replace the word “union” with “interface” here, nothing else changes. Unions and interfaces even match against the same __typename field. Calling it a union instead of an interface presents a communication challenge, API developers must make sure all consumers know that a union may have more/less variants at any point in time. This communication must be made because in other languages unions have great utility by being exhaustive. By calling it an interface and not a union, the assumption that the type is not fixed is encoded in the dialog.

A Union is a way to say "I may return one of a few things at this location" - it should be defined locally to the field. For example, a Video type doesn't need to describe that it might appear alongside Photo for some specific fields, though a specific fields needs to describe that it might return Video | Photo.

An Interface is a way to say "I conform to this contract" - it should be defined locally to the thing which conforms, the Object type. For example, a User type should be able to say that it fulfills the contract of Profile such that any type current known or unknown can be used knowing that it fulfills the contract.

This is a good point, and you’re right the ergonomics of unions are better in this case. However, should ergonomics alone define whether something should be in the spec? Also, your argument here revolves around type definition which is not even part of the spec. GraphQL implementations may choose to allow users to define an interface just as one may define a union. Also, what if an implementation decided to make types open ended? (like in TypeScript)

type A { a: Int }
type B { b: Int }
type C { c: Int }

union U = A | B | C

Desugars to the following in an open ended system:

type A { a: Int }
type B { b: Int }
type C { c: Int }

interface U {}
type A implements U {}
type B implements U {}
type C implements U {}

Here you get the same benefits of local definition with interfaces. Let’s not argue about whether or not defining GraphQL in an open ended manner like this is good or bad, the point of the demonstration is to show that it is up to the GraphQL schema implementor to solve the “I may return one of a few things at this location” case and not the specification. Consumption of the union doesn’t change for the client whether it is a “marker interface” or a union.

I see ergonomics in type definition (which is not even specified!) as the only argument for unions. If we let ergonomics alone define whether unions should be in the spec, that sets a slippery slope precedent.

Another quick note in the case of union SearchResult = Person | Post. I think such a union is an anti-pattern. I personally think the better schema design would be types like so:

interface SearchResult {}

type PersonSearchResult implements SearchResult { person: Person }
type PostSearchResult implements SearchResult { post: Post }

As in the future this allows schema designers to add new fields without making breaking changes at the cost of one inexpensive level of indirection. An example field to be added may be relevance. If this is the best pattern then these pseudo-unions are for all intents and purposes defined locally anyway as most of the time PersonSearchResult and PostSearchResult will generally be defined locally, or at least near, SearchResult.

This concept of contract-less interfaces is often called "marker interfaces." They're typically used either as a backfill for languages like Java that don't have a good concept of Union, or are used as metadata annotations for languages that don't have metadata annotations. I don't think either should apply to GraphQL for modeling data domains.

I think the tradeoff between the bad taste of marker interfaces and simplifying the spec for beginners and tooling is a fine one to make 😊


Anyway, I think I’ve said all I have to say on the matter. I might never use unions, but ultimately I don’t feel too strongly removing them for everyone else. In my opinion over time, unions are only going to make the spec (and subsequently ecosystem) more complicated. So if we can remove them now without breaking a lot of things, why not?

If you haven’t been convinced by any of the arguments above, feel free to close this issue 👍

@leebyron
Copy link
Collaborator

leebyron commented May 1, 2018

Closing this aging issue

@leebyron leebyron closed this as completed May 1, 2018
@kafkahw
Copy link

kafkahw commented Jul 31, 2018

@calebmer this is an interesting idea and I've come across it as well recently. I found that allowing interface to have no fields can be useful, but union would also be needed in some scenarios. For instance, union is more flexible when re-use types. Here's my two cents.

type ObjA {}
type ObjB {}
type ObjC {}

union U1 = ObjA | ObjB
union U2 = ObjB | ObjC

Without union, you'll have to declare more types because each type will be strictly coupled with only one interface.

interface U1 {}
interface U2 {}

type ObjA implements U1 {}
type ObjB implements U1 {}

type ObjB' implements U2 {}
type ObjC implements U2 {}

@kafkahw
Copy link

kafkahw commented Jul 31, 2018

Another use case is when you need to deal with existing types, interface can't help. Say, I need to define a field in a type that could be String, Int or Float. With union I can easily make it like this:

union CustomData = String | Int | Float

type Obj {
   cData: CustomData
}

@IvanGoncharov
Copy link
Member

@kafkahw I'm personally for keeping unions for semantic reasoning but here are a few problems in your arguments:

Without union, you'll have to declare more types because each type will be strictly coupled with only one interface.

You can inherit multiple interfaces:

interface U1 {}
interface U2 {}

type ObjA implements U1  {}
type ObjB implements U1 & U2 {}
type ObjC implements U2 {}

Another use case is when you need to deal with existing types, interface can't help. Say, I need to define a field in a type that could be String, Int or Float.

You can't make unions with scalars only with object types. Even if you have external object types you can always extend it with new interface:

interface UnionImitation {}

extend type SomeExternalType implements UnionImitation

@kafkahw
Copy link

kafkahw commented Jul 31, 2018

@IvanGoncharov thanks for pointing them out!
It's good to know that it supports implementing multiple interfaces and it doesn't support scalar union (which surprises me, I think that's probably why this issue #215 is still open).

@KostaProsenikov

This comment has been minimized.

@robross0606
Copy link

robross0606 commented May 13, 2022

Another place this might come into play is with federated schemas. If you have a federated schema where there is shared interface, that interface can be leveraged by other subgraphs in the federated schema. However, if you have shared union, to my knowledge it is impossible for a subgraph type to "join" that union. If interfaces were allowed to be empty, a federated schema could "join" what is effectively a union without having to be concretely be declared as part of that union

Shared schema:

type ReponseType1 {
  data: ResponseData1
}
union ResponseData1 = Type1 | Type2

type ResponseType2 {
  data: ResponseData2
}
interface ResponseData2 {}

@robross0606
Copy link

robross0606 commented May 13, 2022

Another use case is when you need to deal with existing types, interface can't help. Say, I need to define a field in a type that could be String, Int or Float. With union I can easily make it like this:

union CustomData = String | Int | Float

type Obj {
   cData: CustomData
}

When did GraphQL start allowing unions of scalar types? To my knowledge, this example is not possible.

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

No branches or pull requests

6 participants