graphql-spec icon indicating copy to clipboard operation
graphql-spec copied to clipboard

RFC: Type composition

Open wiegell opened this issue 3 months ago • 12 comments

Background:

  • https://github.com/graphql/graphql-spec/issues/533
  • https://github.com/graphql/graphql-spec/issues/500

The two proposals above discuss the option on how to write GraphQL in a more DRY fashion. Some concerns raised in those topics include:

  • The GraphQL language is intended for reading over writing
  • We should preserve backwards compatibility at all costs.

Proposal

If instead of changing the way interfaces work and/or are implemented, what if we instead enabled type composition much like Go does it. This could then be an example:

type Humanoid {
  id: String
  name: String
  appearsIn: [Episode]
}

type Human {
  Humanoid
  homePlanet: String
}

type Droid {
  Humanoid
  primaryFunction: String
}

Or multiple types could be combined:

type TimelineItem {
  date: String!
}

type Identifiable {
  id: ID!
  name: string
}

type SomeTimelineEvent {
  TimelineItem
  Identifiable
  eventName: string
}

There are some details that would need to be discussed in particular how field collisions should be handled. I don't see any real breaking changes in this or problems with backwards compatibility and it would let the typing become more DRY - let me know what you think.

wiegell avatar Oct 15 '25 11:10 wiegell

@wiegell thank you for opening this ❤️ I'd love to see something like this happening. Do you want to add this to the working group next week to discuss how to best approach there? If yes, please add your name and a 10min item to https://github.com/graphql/graphql-wg/blob/main/agendas/2025/11-Nov/06-wg-primary.md

martinbonnin avatar Oct 29 '25 11:10 martinbonnin

hi @martinbonnin, thank you for the kind response. I did not get back to this in time i see, i also just have to the the signed agreement through my employer. I will try and sign up for a future meeting

wiegell avatar Nov 06 '25 13:11 wiegell

@wiegell , there's a working group next week. Would be cool to see you there! You can read here for how to add yourself to the agenda. Let me know if you have any question!

martinbonnin avatar Nov 26 '25 15:11 martinbonnin

@martinbonnin i am sorry i am not able to participate this week, but i have made a PR for the agenda in January 26

wiegell avatar Dec 02 '25 12:12 wiegell

@wiegell can you share the PR? For some reason I can't find it.

martinbonnin avatar Dec 02 '25 13:12 martinbonnin

@wiegell can you share the PR? For some reason I can't find it.

Sure! https://github.com/graphql/graphql-wg/pull/1865

wiegell avatar Dec 02 '25 14:12 wiegell

Thanks! Heads up this is on January 15th, not 26th. It's the secondary wg, which is usually less attended but I suspect the 1st of January will be cancelled so all good 👍

martinbonnin avatar Dec 02 '25 14:12 martinbonnin

TY, yes i just meant January, 2026, that came out wrong i see now! :)

wiegell avatar Dec 02 '25 16:12 wiegell

Ahhh, I see 👍 I think I'm just tired 😅 All good, Thanks! ✅

martinbonnin avatar Dec 02 '25 16:12 martinbonnin

I do not like this syntax at all; it could easily be that you've added a field and forgotten to define the type; or simply that you forgot a :

type SomeTimelineEvent {
  TimelineItem
  Identifiable
  eventName: string
}

It also makes it look like those types are nested inside of the type, rather than it implementing those interfaces. (And if they're nested inside... then why not just timelineItem: TimelineItem which already works?)

Here's an alternative that makes the intent a lot clearer by reusing a syntactic production we already have:

type SomeTimelineEvent extends TimelineItem & Identifiable {
  ...TimelineItem
  ...Identifiable
  eventName: string
}

benjie avatar Dec 04 '25 16:12 benjie

I like the idea behind this (modified) proposal, however there are some things to be aware of:

  1. If the definition of a field on an interface changes, for example by adding an (optional) argument, then all downstream types automatically state that they implement this argument. This should typically require a stronger assertion; but this is development tooling so I'm not too worried.

  2. If your type wishes to have a variant of a field, as is your right when implementing an interface (for example, by supporting an additional optional parameter), then you would need to unwind the full spread and define each field manually.

  3. If two different interfaces are implemented, and each share a field with different optional arguments, the result is less clear (interface A { field(foo: Int) } interface B { field(bar: Int) } type T implements A & B { ...A ...B })

  4. In the simple case (avoiding the above complexities) here's two different ways of expressing the same type - one where the fields are all listed, and one where it only spreads the interfaces. Should GraphQL automatically detect the commonality and output the spread form? Should the spread form only be used for composition, but the resultant SDL always lists the fields in full?

  5. Order of fields can be significant; using the type spreads would impact this. So even if a field uses all of the fields of an interface, implements all the arguments and descriptions identically, that doesn't mean we can use the spread form unless they're all defined a) in the same order, and b) without any other fields defined between them. It gets a lot harder to determine whether a field list can be turned into spreads or not.

  6. Extension to the above: does this mean we need to have literal introspection of whether the spread is used vs manual field definition? How would you represent this in introspection? How would this remain backwards compatible with clients expecting a list of fields?

benjie avatar Dec 04 '25 16:12 benjie

And I should mention that literal embedding, as in Go, already works - simply:

type Humanoid {
  id: String
  name: String
  appearsIn: [Episode]
}

type Human {
  humanoid: Humanoid
  homePlanet: String
}

type Droid {
  humanoid: Humanoid
  primaryFunction: String
}

benjie avatar Dec 04 '25 16:12 benjie