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

Explore possibility of generic types #190

Open
AndrewIngram opened this issue Jun 28, 2016 · 70 comments
Open

Explore possibility of generic types #190

AndrewIngram opened this issue Jun 28, 2016 · 70 comments
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)

Comments

@AndrewIngram
Copy link

AndrewIngram commented Jun 28, 2016

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.

@leebyron
Copy link
Collaborator

leebyron commented Jul 2, 2016

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
Copy link
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
Copy link

Qard commented Mar 20, 2017

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
Copy link
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
Copy link

Qard commented Mar 20, 2017

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
Copy link
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
Copy link

Qard commented Mar 20, 2017

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
Copy link
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
Copy link
Contributor

mike-marcacci commented May 9, 2017

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
Copy link
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
Copy link
Contributor

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

@AlecAivazis
Copy link

AlecAivazis commented May 18, 2017

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
Copy link

xialvjun commented Jul 7, 2017

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
Copy link

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.

@iamdanthedev
Copy link

Addressing problems @leebyron mentioned in the 2nd post here, generics don't need to appear in introspection at all as they are abstract helpers. Concrete types extending generics could be introspected with all the precision of graphql

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

type Person {
    id
    name
    # ...
}

type PaginatedPersons extends Pagination<Person> {
  extraField: Int! // maybe
}

type Query {
    search_person(limit: Int = 20, offset: Int = 0, q: String): PaginationPersons
}

In this case the type PaginatedPersons could have in introspection this shape

type PaginatedPersons {
    total: Int!
    limit: Int!
    offset: Int!
    results: [Person!]!
    extraField: Int!
}

@dallonf
Copy link

dallonf commented Nov 10, 2017

I think there is a potential use case for client-side generics in enabling re-usable components. Example, using react-apollo, some hypothetical syntax to inject a fragment into another fragment, and the Pagination<T> type from the above post:

const PaginatedListFragment = gql`
# some purely hypothetical syntax on how you might inject
# another fragment into this one
fragment PaginatedList($itemFragment on T) on Pagination<T> {
  total
  offset
  limit
  results {
    id
    ...$itemFragment
  }
}
`;

const PaginatedList = ({ data, renderItem, onChangeOffset }) => (
  <div>
    {data.results.map(item => <div key={item.id}>{renderItem(item)}</div>)}
    <PaginationControls
      total={data.total}
      offset={data.offset}
      limit={data.limit}
      onChangeOffset={onChangeOffset}
    />
  </div>
);

const PeoplePageQuery = gql`
query PeoplePage($limit: Int, $offset: Int) {
  people(limit: $limit, offset: $offset) {
    # again, hypothetical parametric fragment syntax
    ...PaginatedList($itemFragment: PersonItemFragment)
  }
}
${PaginatedListFragment}

fragment PersonItemFragment on Person {
  id
  name
}
`;

const PeoplePage = ({ data }) => (
  <div>
    <h1>People</h1>
    <PaginatedList
      data={data.people}
      renderItem={item => <a href={`/person/${item.id}`}>{item.name}</a>}
      onChangeOffset={offset => data.setVariables({ offset })}
    />
  </div>
);
]

This would be something very powerful for component-based frameworks!

@AndrewIngram
Copy link
Author

@dallonf this was actually one of the other benefits I envisaged :)

It's the primary reason why I don't think it's enough for this to just be syntactic sugar for the existing capabilities, allowing clients to know about generics could be a very powerful feature.

@kbrandwijk
Copy link

I also have another use case for generics. I want to define an array of distinct elements, commonly knows as a Set. So instead of defining a field like myValues: [String], I would like to define it as myValues: Set<String> to accomplish this.

@mike-marcacci
Copy link
Contributor

mike-marcacci commented Nov 28, 2017

To add another use case, I've found myself making custom scalars when I would otherwise define a Map<SomeType>. In these cases SomeType has always been a scalar so this has been acceptable, but there have been other times when I chose to use [SomeObjectType] when a real Map<SomeObjectType> would have been preferable.

-- edit --

I do want to note, though, that this would be quite a challenge to implement, since the generic's definition in the schema would need to fully describe the final structure. Otherwise, there's no way for a client to know what to expect.

@axos88
Copy link

axos88 commented Feb 15, 2018

Another use, also tied with pagination is when different interfaces need to be paginated.

Suppose we have an interface Profile, and two implementations: UserProfile and TeamProfile

Without generics, I cannot see a solution to be able to deduct that UserProfilePagnated and TeamProfilePaginated can be supplied where we are expecting PaginedProfiles.
With generics - although not trivial, one can make a complex analyzer that understands the notion of co and contravariance, and can deduct that PaginatedProfiles is an interface of UserPaginatedProfiles, although one might need to signal this somehow.

@mike-marcacci
Copy link
Contributor

Hi @axos88, can you add your comment on #295? Your use case is exactly the kind of problem it is designed to address.

@axos88
Copy link

axos88 commented Feb 15, 2018

@mike-marcacci I'm not sure how interfaces implementing interfaces is enough to have a solution for the problem

@axos88
Copy link

axos88 commented Feb 15, 2018

Although I agree, the problem is related.

@axos88
Copy link

axos88 commented Feb 15, 2018

Oh I see. :)

@akomm
Copy link

akomm commented Mar 16, 2018

@leebyron

What if generic types can not be returned by fields without type specification. They are just for boilerplate reduction. As you can see in the following definition (from @AndrewIngram's post) the field bars does not return the generic type over T, but Bar:

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

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

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

And the generic types are not visible in introspection.

Result of this could be:

type ConnectionEdgeBar {
   node: Bar
   cursor: String
}

type ConnectionBar {
   edges: ConnectionEdgeBar
   pageInfo: PageInfo
}

type Foo {
   id: ID!
   bars: ConnectionBar
}

As far as I know type name duplication is not allowed in graphql schema and all implementation raise errors in case of duplication. So when you already have ConnectionBar somewhere, then you would get error.

You do not have to drop the information when schema is parsed, so you could point out that ConnectionBar being generated from generic Connection<T> is already defined. In introspection the schema is already processed and the there only exists the ConnectionBar.

I can't speak for others, but I have a lot of boilerplate, which would be saved if I could use generics like that.
I have noticed, that on field-level (except generic type fields) I do not need to be generic in this case. I can defined specific type there. No need to return generic<T,V> etc.

@fbartho
Copy link

fbartho commented Mar 16, 2018

@akomm's recommendation is the style that I've gone with in my project.

I currently implemented it with @generic(...) as a directive, that I preprocess out to generate the types:

interface Page @generic(over: [DataType]) {
	items: [DataType]
	meta: PageMeta
}

type PageMeta {
	page: Int
	totalPages: Int
	totalItems: Int
}

# Elsewhere:

type PopularItems_ implements Page
	@generic(using: [PopularItem_]) {
	searchSectionTitle: String!
}

type PopularItem_ {
	id: ID!
	title: String!
	iconUrl: String
	narrowPhotoUrl: String!
	slug: String!
}

After pre-processing, the fields from the Page interface are merged in to the fields of PopularTemplates_ after distributing the using-parameter over all instances of DataType

@wtrocki
Copy link

wtrocki commented Apr 1, 2020

Based on quick investigation I think could be a couple of approaches to resolving the problem with the type name

  1. Preprocessor/Templating

Having extra type defined in the parsed schema object that is not defined in the SDL. This brings a number of challenges and probably it is not suitable for the spec implementation as it forces developers to use types that are not defined in SDL. From my point of view this could be the way how the community can implement generics now without involving changes in the spec.

An example was mentioned above:
#190 (comment)

Pure hack for syntax using existing spec could look like this

type Page @generic {
  offset: Number
  total: Number
  """ items: [GenericField] """
}

type User @createGenericArray(name: "items", wrappingType: "Page") {
  id: ID
}

type Query {
  getUsers: UserPage
}

Result schema after processing could look like this:

 type User {
  id: ID
 }

type  UserPage {
  offset: Number
  total: Number
  items: [User]
}

type Query {
  getUsers: UserPage
}

This is a pure hack but I have used this to evaluate options and see some challenges

  1. Explicit type definitions for generics

Any usage of generics could require an explicit definition of the type that do not have generics

Explained already in a couple of comments in this issue
#190 (comment)
and
#190 (comment)

Summary

For some of the user cases, code generation can be employed to add some form of templating capabilities to schema and generate schema that contains types explicitly defined (For example https://www.opencrud.org). This is a pretty well-known workaround for the moment.

As for the spec having the ability to define an explicit type for generics could resolve this problem on the spec level - although it could look too verbose.

@justinfagnani
Copy link

I'm just learning GraphQL, but already I'm seeing the need for generic types in my use cases. In particular I need to deal with a Reference type in my backend, where the value can reference any other type. There can be generic fetch operations that can retrieve the value of a ref.

Something like so:

type User {
  manager: Ref<User>;
}

type Query {
  getRef(ref: Ref<T>): T
}

@benjie
Copy link
Member

benjie commented Dec 7, 2020

@justinfagnani That seems like a use case for interfaces or unions.

@xialvjun
Copy link

xialvjun commented Apr 19, 2021

I don't think it's a tech problem for not implementing this for 5 years.
It's just there is no one can decide should we make it.
@alamothe is right.

@dncrews
Copy link

dncrews commented Jun 7, 2021

Just to add a use-case that I've been struggling with, which is the "other side" of "discoverable query successes" (Connections) is "discoverable mutation failures":

Mutation Errors

In the schema, we're trying to call out all of the potential failure cases when they're "the user can do something to fix this, it's not a critical failure". To do this, we're introducing an error code that is an enum rather than a string. This one change makes what would've been a shared generic response type into triple the amount of schema. It feels wrong that making my schema self-documenting makes it significantly noisier.

Here's what I wish I could do:

interface MutationError<CodeEnum> {
  path: [String!]
  message: String!
  code: <CodeEnum>!
}

enum USER_CREATE_ERROR_CODE {
  USERNAME_TAKEN
  PASSWORD_REQUIREMENTS_NOT_MET
  …
}

interface MutationFailure<CodeEnum> {
  errors: [MutationError<CodeEnum>!]!
}

type UserCreatePayload = UserCreateSuccess | MutationFailure<USER_CREATE_ERROR_CODE>

type Mutation {
  userCreate(…): UserCreatePayload!
}

With the request

mutation CreateUser {
  userCreate(…) {
    ... on UserCreateSuccess {
      ... UserInfo
    }
    ... on UserCreateFailure {
      errors {
        path
        message
        code
      }
    }
  }
}

Adding extra pain to this is this related enum issue, so I can't even do this with interfaces at all, as you'd expect.

@akomm
Copy link

akomm commented Jun 8, 2021

GraphQL libraries in different languages usually provide at least two methods of defining the schema.

  1. Using graphql semantics to write the types and a resolver map that maps fields
  2. Using some sort of alternative configuration format, like yaml or write code to create schema

I'd prefer the first method, but actually use the latter one. The reason hereby is the lack of abstraction/generics. With code solution I can write a factory that creates a non-generic type using arguments representing generics. This way I can abstract.

Would be nice if I could use the 1. method without losing the abstraction

@fluidsonic
Copy link
Contributor

We also lack generics in various situations, like this one:

type Change<Value> {
   old: Value
   new: Value
}

type Contact {
   companyName: String
   firstName: String!
   id: ID!
   lastName: String!
}

type ContactUpdatedJournalItem {
   companyName: Change<String>
   firstName: Change<String!>
   id: ID!
   lastName: Change<String!>
   // etc.
}

We commonly use this pattern to create journal items for an object's change history.

  • For each property of the object we log a change in its Change<T> property.
  • If there is no change, then Change<T> is null.
  • Changes on a nullable T can be { old: "foo", new: null }, { old: null, new: "foo" } and { old: "foo", new: "bar" }.
  • Changes on a non-nullable T! can only be { old: "foo", new: "bar" }.

For us it's important that the use-site of the generic type specifies if T is nullable or not.

But there may be other scenarios. The question is what are the possible combinations and use cases?

type Foo<T> {
   t1: T      // String or String! for Foo<String!>  ??
   t2: T!     // String!
   t3: T?     // String   (if nullability of T can be stripped)
}

type Bar {
   foo1: Foo<String>
   foo2: Foo<String!>
}

And should it be possible to force non-null types?

type Foo<T!> { // must be non-nullable
   t1: T      // String, String!, or forbidden  ??
   t2: T!     // String!
}

type Bar {
   foo1: Foo<String>   // not allowed
   foo2: Foo<String!>  // ok
}

Regarding other generic scenarios, similar to edges, we have something like this in Kotlin:

class SearchResult<out Element : Any>(
    val elements: List<Element>,
    val nextOffset: Int?,
    val totalCount: Int,
)

class SearchQueryOutput(
    val cityResult: SearchResult<City>?,
    val contactResult: SearchResult<Contact>?,
    val locationResult: SearchResult<Location>?,
    val processResult: SearchResult<Process>?,
)

Our own Kotlin GraphQL library translates it to something like this:

type CitySearchResult {
    elements: [City]!
    nextOffset: Int
    totalCount: Int!
}

type ContactSearchResult {
    elements: [Contact]!
    nextOffset: Int
    totalCount: Int!
}

type LocationSearchResult {
    elements: [Location]!
    nextOffset: Int
    totalCount: Int!
}

type ProcessSearchResult {
    elements: [Process]!
    nextOffset: Int
    totalCount: Int!
}

type SearchQueryOutput {
    cityResult: CitySearchResult
    contactResult: ContactSearchResult
    locationResult: LocationSearchResult
    processResult: ProcessSearchResult
}

The transformation automatically specializes generic types. That quickly blows up the GraphQL type system and makes it quite cumbersome to use on the client side. It also makes the server's GraphQL library implementation more complicated.

@fluidsonic
Copy link
Contributor

Regarding the templating approach it quickly gets out of hand when Lists and NonNull types are involved.
From my example above, there might be:

  • Change<String>
  • Change<String!>
  • Change<[String]>
  • Change<[String!]>
  • Change<[String]!>
  • Change<[String!]!>

Which would expand to an increasingly weird type system, e.g.:

type StringChange { … }
type NonNullStringChange { … }
type NonNullStringChange { … }
type StringListChange { … }
type NonNullStringListChange { … }
type StringNonNullListChange { … }
type NonNullStringNonNullListChange { … }

(add more List nesting for more fun)

Another question: Would it be allowed to use generic types as a generic argument?

type Foo<T> {
   foo: T
}

type Bar<T> {
   bar: T
}

type Baz {
   baz: Bar<Foo<String>>
}

// { baz: { bar: { foo: "xyz" } } }

@n614cd
Copy link

n614cd commented Jul 7, 2021

In many ways GraphQl lends itself to data driven architectures; except the lack of generics, it pretty much kills the ability to use a common set of queries and mutations. Technically, you can almost solve this with interfaces.

In my current system, I have roughly fifty entities, which are flattened down to twenty five objects consumed by the UI. The effectively means instead of four query/mutations for manage and perform all CRUD, I have a hundred. (search, create, update and delete per UI object). This makes the API very unwieldy, and is before we get into any transactional specific APIs.

Tim

@dan-turner
Copy link

Any progress or developments on this?

@ghost
Copy link

ghost commented Nov 5, 2022

I would recommend going off of the TS Spec because it has almost everything GraphQL needs for generics.

Regarding implementation, I think I have an idea for a new typed API. Here's how it would look in JavaScript:

const Change = GQLAlias('Change', ['T'], T => GQLObject([
    GQLProperty('old', T.NotNull()),
    GQLProperty('new', T.NotNull()),
]))

// usage:
const schema = Schema([
    Property('getLatestChange', Change(GQLString)),
    // ...
])

That is just an idea; I am going to try to implement a better schema/resolver API for a smaller library to test this example.

Also, I recommend replacing the ! operator with a sort of Maybe<T> for better type safety.

@rgolea
Copy link

rgolea commented Mar 7, 2023

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
Copy link

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
Copy link

akomm commented Mar 10, 2023

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
Copy link

jamietdavidson commented Sep 7, 2023

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
Copy link

n614cd commented Jan 10, 2024

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

@varvay
Copy link

varvay commented Feb 17, 2024

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
Copy link

mathroc commented Feb 19, 2024

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]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💭 Strawman (RFC 0) RFC Stage 0 (See CONTRIBUTING.md)
Projects
None yet
Development

No branches or pull requests