Skip to content

Explore possibility of generic types #190

Open
@AndrewIngram

Description

@AndrewIngram

As projects like Relay have shown, it's relatively common to repeat the same generic structures of types multiple times within a project. In the case of Relay, I'm talking about Connections.

The GraphQL definition language already has explicit support for one particular form of generic type, arrays:

type Foo {
   id: ID!
   bars: [Bar]
}

I'd like to start discussion about being able to do something similar for user-defined structures:

generic ConnectionEdge<T> {
   node: T
   cursor: String
}

generic Connection<T> {
   edges: ConnectionEdge<T>
   pageInfo: PageInfo
}

type Foo {
   id: ID!
   bars: Connection<Bar>
}

The overall goal is to reduce the amount of boilerplate in creating schemas with repetitive structures.

Activity

leebyron

leebyron commented on Jul 2, 2016

@leebyron
Collaborator

Thanks for proposing this! During the GraphQL redesign last year, we actually considered adding this!

There are two issues that seemed to result in more complication than would be worth the value we would get from this feature, but I'm curious what you think or if you have ideas:

  1. What does the introspection result look like? When I read the ConnectionEdge type and look at its fields, what will the type of node be? The best answer we could come up with was to change Field.type from Type to Type | GenericParameter which is a bit of a bummer as it makes working with the introspection API more complicated. We could also expand Type to include the possibility of defining a generic param itself. Either way, it also has some rippling effects on the difficulty of implementing GraphQL, which would need to track type parameter context throughout most of it's operations.
  2. What should __typename respond with? What should { bars { __typename } } return? This one is pretty tricky. { "bars": { "__typename": "Connection" } }? That describes the type, but you're missing info about the type parameter, that that ok? { "bars": { "__typename": "Connection<Bar>" } } Is also problematic as now to use the __typename field you need to be able to parse it. That also adds some overhead if you were hoping to use it as a lookup key in a list of all the types you know about.

Not to say these problems doom this proposal, but they're pretty challenging.

Another thing we considered is how common type generics would actually be in most GraphQL schema. We struggled to come up with more than just Connections. It seemed like over-generalization to add all this additional complexity into GraphQL just to make writing Connections slightly nicer. I think if there were many other compelling examples that it could motivate revisiting.

AndrewIngram

AndrewIngram commented on Jul 2, 2016

@AndrewIngram
Author

You're right about the number of use cases being relatively small, i'll need to think on that point.

To be honest, this feels like sugar for developers of schemas rather than clients. In the simplest case, i'd just expect the introspection result to be the same as it is now, i.e the generics get de-sugared. To that end, it could just be something that parsers of the schema definition language end up supporting, but it's up to library authors how to handle the generated AST.

In graphql-js land, there are numerous examples of libraries (apollo-server, my own graphql-helpers, and a few others I can't remember) which use the parser provided to vastly simplify the process of building schemas (having done it both ways, I'd say it's pretty close to an order of magnitude more productive), and i'd personally be happy to add additional support for tokens related to generics to my library.

However, it does feel weird supporting a syntax that's not actually reflected in the final generated schema, so i'm unsure about this approach.

Qard

Qard commented on Mar 20, 2017

@Qard

I really wish something like this would be reconsidered. Connections may just be a single use-case, but it's a big one, in my opinion. The length of my current schema would cut in half with generics.

Currently I have 24 copies of basically this:

type TypeXConnectionEdge {
   node: TypeX
   cursor: String
}
type TypeXConnection {
   edges: TypeXConnectionEdge
   pageInfo: PageInfo
}

That's nearly 200 lines of code that could easily be expressed in 8 lines of generics. I'm seriously considering writing my own preprocessor just to hack on my own generics capability...

stubailo

stubailo commented on Mar 20, 2017

@stubailo
Contributor

Hmm, in graphql-tools you could do something like:

type MyType {
  hello: String
  world: String
}

${ connectionAndEdgeFor('MyType') }

Is there something the syntax could have that would be better than that JS-style approach?

Qard

Qard commented on Mar 20, 2017

@Qard

And what if you're not using JS? 😞

I want my schema to be pure graphql schema language so it doesn't need preprocessing.

stubailo

stubailo commented on Mar 20, 2017

@stubailo
Contributor

Yeah I definitely sympathize. I guess the real question is, is the generic thing just something for the server to be written more conveniently, or does the client somehow know that these types/fields are generic and acts accordingly?

If the client doesn't know, then I feel like it should be a preprocessor feature or a macro thing. The spec is all about the contract between client and server IMO.

However, there are definitely implementations for generics where the client could actually take advantage of knowledge that something is a generic thing. For example, in the connection example, there's no way to make an interface that says that a TypeXConnectionEdge should have a node of type X, so you can't really enforce that without relying on conventions.

Perhaps this could be done as some sort of intersection of interfaces and type modifiers? So basically, it's a way of creating your own type modifiers - if you squint hard enough, [ ... ] and ! are kind of like List<T> and NonNull<T>.

So building on that, in introspection:

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
  }
}

You could get:

{
  kind: "GENERIC",
  name: "Connection",
  ofType: {
    kind: "OBJECT",
    name: "Photo"
  }
}

Perhaps this should come with a change of kind: "LIST" to kind: "GENERIC", name: "LIST".

Qard

Qard commented on Mar 20, 2017

@Qard

I mean, it seems hugely valuable for the client to understand generic concepts too, but that could probably be expressed as simply differently named types, since the client doesn't generally need to be too concerned about the actual name of types so much as what is in them. It seems to me like it'd be really valuable to be able to, in a type-safe way, express concepts like pagination while retaining the safety of recognizing that a given type encapsulates objects of a specific type. Without generics or preprocessing you can sorta-kinda do this with unions, but then you're throwing away the promise that all items in a list are of a specific type...

stubailo

stubailo commented on Mar 20, 2017

@stubailo
Contributor

I guess in my mind, whether or not the client should worry about it is a very important factor in determining whether it should be in the spec or simply live as some server-side library tooling.

mike-marcacci

mike-marcacci commented on May 9, 2017

@mike-marcacci
Contributor

Hi everybody! I've definitely been looking to solve the Connection use-case here, and another similar case specific to my project. I have a slightly different strategy which I've laid out in #295, which is a much smaller change, and in no way mutually exclusive with this proposal.

Basically, if an interface were able to explicitly implement another interface, the client would be aware of the hierarchy. That is, it would be provided similar context to what generics might provide, but without adding new semantics to the protocol.

This wouldn't solve the issue of verbosity within the schema, but leverage the existing types to convey "generic" type information to the client. In this way, a preprocessor might be able to compile generics into hierarchal interfaces, achieving both goals here.

AndrewIngram

AndrewIngram commented on May 10, 2017

@AndrewIngram
Author

Given that we now have at least one authoring-only syntax extension, i.e. extend type, is it worth reconsidering generics in the same light?

stubailo

stubailo commented on May 11, 2017

@stubailo
Contributor

Hmmm, that's true - extend type is not accessible through introspection at all, I didn't think about that.

AlecAivazis

AlecAivazis commented on May 18, 2017

@AlecAivazis

One use case I have run into for generics is having something resembling Maybe<T> to handle error messages. I'd like to do so without having to mess with the network layer and introduce some sort of global state, or refer to a disjointed part of the response. Currently, I am defining a separate union type for each object (ie MaybeUser is a User | Error) but it would be nice to be able to do this as simply as Maybe<User> and define the structure once.

An alternative to avoid the extra complexity of the generic union would be something as simple as

generic Maybe<T> {
   left: Error
   right: T
}
xialvjun

xialvjun commented on Jul 7, 2017

@xialvjun

Another use case is

generic Pagination<T> {
    total: Int!
    limit: Int!
    offset: Int!
    results: [T!]!
}

type Person {
    id
    name
    # ...
}

type Query {
    search_person(limit: Int = 20, offset: Int = 0, q: String): Pagination<Person>!
}
crypticmind

crypticmind commented on Aug 5, 2017

@crypticmind

generic Maybe<T> { ... } and generic Pagination<T> { ... } look great to me, though I'd drop the generic keyword as it seems redundant by the use of angle brackets.

60 remaining items

dan-turner

dan-turner commented on Oct 1, 2022

@dan-turner

Any progress or developments on this?

ghost
rgolea

rgolea commented on Mar 7, 2023

@rgolea

I can't wait for this to become a reality. It's the main reason I had to move away from graphql as it become so hard to maintain and work with. I had to remember all these properties I had to distribute everywhere. And yes, implements works pretty well but at the end of the day I had to copy all the properties around.

However, thank you all for the awesome contribution. I can't stress enough how much I love graphql.

DeveloperTheExplorer

DeveloperTheExplorer commented on Mar 9, 2023

@DeveloperTheExplorer

It has been almost 7 years. Any updates? This is very much needed for schema/resolver definitions. Almost all endpoints need this whenever pagination is involved. And almost all systems implement some sort of pagination/cursor.

akomm

akomm commented on Mar 10, 2023

@akomm

Going schema DSL in the graphql implementations (libs, not gql itself) is one of the biggest mistakes IMO. You can see it by all those "solutions" cooked around it by now, to fix problem A, B, C and problems still being present to this day. Whoever stayed on the code side without all the mess around it, has much less problems with graphql.

Like always, I try out new tech and evaluate it. It feels tempting at first glance, but the price vs. benefit is hugely disproportional.

You can have quite clean schema definitions also in code. Without all the primitive constructors provided by the impl. libraries. And you can eliminate repetition that you try to solve with generics in schema, just by using code.

The examples I've seen so far here, that should prove the code approach without generics leads to bad naming, feels artificial. Most of the time its a matter of Change<String> vs. ChangeString. The example variants with nullability are not really useful. To talk about whether its a problem or not needs some real world example to see whether its even needed for the type in the specific case to have the nullability encoded in the name this way. I can just imagine RL examples for that where you don't actually need it, unless you make some weird design decision. If you want to avoid collision in name, the first question is why do you have those two variants, what is the intent of the data structure you try to describe and isn't the nullability something that is rather implied from a different type name as a base (OptionalChange<T> vs. Change<T>) instead.

I'm not saying there is no problem, but just that the examples I've seen here so far are IMO to artificial to be convincing.

jamietdavidson

jamietdavidson commented on Sep 7, 2023

@jamietdavidson

Would like to reup this. Seems relevant and I stumble into Enum / Interface related issues every few months that would be solved by this. It's kind of the last missing piece to making GraphQL not leaving something to be desired, IMO.

n614cd

n614cd commented on Jan 10, 2024

@n614cd

Is there a specific blocker to making this happen?
Resources to push it through? Some unsolved problem?

varvay

varvay commented on Feb 17, 2024

@varvay

At first I was excited to the idea about generic type support in GraphQL, since it might introduces high degree of Intuitiveness for developer to translate the GraphQL schema into code implementation in various language and framework. It's the same concept as having scalar data types as much as possible to match all the possible implementor languages e.g., think how intuitive is it to implement the schema when it supports data type like hashmap, JSON node, etc.

But I've been thinking to myself about this and end up by accepting the current GraphQL specification without generic type support. The question i've been asking myself are,

  • are the issue regarding the need of generic type should be handled by the specification or the implementation (code implementation and framework)?

So I started from the mindset that GraphQL is a specification used as contract between frontend and backend on what data they will exchanging and how are they structured. There will be complication introduced with generic type implementation on these information received by the client, for example how does the client knows the structure of the object defined as generic? There must be an information to communicate it right? you'll ended up by sending some kind of metadata, which is redundant to __typename.

The next question is,

  • how generic type support in the specification helps us in the system design and implementation process? Is it by increasing the quality of the information delivered to the client? Is it decreasing the complexity of these process significantly?

I don't think so. The client's frameworks will still need to implement the data resolution abstraction and the quality of the information received by the client doesn't necessarily increased since the only additional information are "this object is generic and it might be in type A, B or C", which is communicated already through the usage of union type definition.

  • can't the current specification fulfill the abstract needs for generic types e.g., pagination and error-messaging?

I think it does fulfill them with two possible solutions on the table,

  1. using interface.
  2. using union data type, which is the one I prefer.

Discussing the 2nd solution further,

type Post {
   # some type definition
}

type Profile {
   # some type definition
}

type Tag {
   # some type definition
}

type Setting {
   # some type definition
}

union Pageable = Post | Profile | Tag | Setting # Verbosity potential

type PagedResult {
   data: [Pageable]
   page: Int
   # some other pagination related definition
}

type Query {
   getPosts(): PagedResult
   getProfiles(): PagedResult
   getTags(): PagedResult
   getSettings(): PagedResult
}

Such query is resolvable in the code implementation by mapping the type based on the __typename metadata, for example like using discriminated union in typescript. I also still consider the schema definition verbosity is acceptable since the only verbosity came from Pageable union type definition, since for new type extending pagination functionality will goes to this list. But the tradeoff with strong data typing and information quality build upon the contract are reasonable.

Or what you really asking is some kind of no-type data and omitting the type enforcement feature? I think this is oppose to the main goal of GraphQL itself.

Finally, based on those thinking, I concluded for myself that in use cases I've seen so far, the only benefit I'm going after is syntactic sugar. This might also be your case. In my opinion, If there are any work should be done regarding this, they should be on the client side (developer and implementor framework).

mathroc

mathroc commented on Feb 19, 2024

@mathroc

for example how does the client knows the structure of the object defined as generic? There must be an information to communicate it right? you'll ended up by sending some kind of metadata, which is redundant to __typename.

no, if you have:

type Post {
  name: String!
}

type List<T> {
  nodes: [T!]!
}

type Query {
  posts: List<Post>
}

then there is no doubt about what posts.nodes contains in query { posts { nodes {}}}

the only additional information are "this object is generic and it might be in type A, B or C"

This is not what generics do

and it's probably the same misunderstanding, but if you use generics in your example, it becomes:

type Post {
   # some type definition
}

type Profile {
   # some type definition
}

type Tag {
   # some type definition
}

type Setting {
   # some type definition
}

type PagedResult<T> {
   data: [T]
   page: Int
   # some other pagination related definition
}

type Query {
   getPosts(): PagedResult<Post>
   getProfiles(): PagedResult<Profile>
   getTags(): Paged<Tag>
   getSettings(): PagedResult<Setting>
}

And there's definitely readability improvements as-well as better typings than in the previous version (you know that query.getPosts.data will always be of type [Post], not [Post|Profile|Tag|Setting]

nort3x

nort3x commented on May 10, 2024

@nort3x

@mathroc

inspired by how C++ resolve templates, we can preprocess schema and produce this pipe:

input schema:

type PagedResult<T> {
   data: [T]
   page: Int
}

type Query {
   getPosts(): PagedResult<Post>
   getProfiles(): PagedResult<Profile>
}

processed schema:

type PagedResultPost{
   data: [Post]
   page: Int
}

type PagedResultProfile{
   data: [Profile]
   page: Int
}

type Query {
   getPosts(): PagedResultPost
   getProfiles(): PagedResultProfile
}

notice that we can't really mangle the outcome because it should be follow-able by consumer of the API
another issue is that the consumer should also use the same processor or re-implement them one by one

i used this in a small passion project and wasn't really an issue, but i understand the complication and integrity problems it could bring into the specification...

8 years is alot i think graphql specification should stay as is and task these improvements to it's successors

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @AndrewIngram@leebyron@fluidsonic@benjie@Qard

        Issue actions

          Explore possibility of generic types · Issue #190 · graphql/graphql-spec