-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Comments
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):
One of two things can happen: First, a type could be removed. Perhaps
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 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 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. |
If you replace the word “union” with “interface” here, nothing else changes. Unions and interfaces even match against the same
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 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
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 👍 |
Closing this aging issue |
@calebmer this is an interesting idea and I've come across it as well recently. I found that allowing type ObjA {}
type ObjB {}
type ObjC {}
union U1 = ObjA | ObjB
union U2 = ObjB | ObjC Without interface U1 {}
interface U2 {}
type ObjA implements U1 {}
type ObjB implements U1 {}
type ObjB' implements U2 {}
type ObjC implements U2 {} |
Another use case is when you need to deal with existing types, union CustomData = String | Int | Float
type Obj {
cData: CustomData
} |
@kafkahw I'm personally for keeping unions for semantic reasoning but here are a few problems in your arguments:
You can inherit multiple interfaces: interface U1 {}
interface U2 {}
type ObjA implements U1 {}
type ObjB implements U1 & U2 {}
type ObjC implements U2 {}
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 |
@IvanGoncharov thanks for pointing them out! |
This comment has been minimized.
This comment has been minimized.
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 {} |
When did GraphQL start allowing unions of scalar types? To my knowledge, this example is not possible. |
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:
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:
Node
facebook/relay#1203, __typename is null when an optimistic update is applied facebook/relay#1230)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 😉
The text was updated successfully, but these errors were encountered: