-
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
Explore possibility of generic types #190
Comments
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:
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. |
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. |
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:
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... |
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? |
And what if you're not using JS? 😞 I want my schema to be pure graphql schema language so it doesn't need preprocessing. |
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 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, So building on that, in introspection:
You could get:
Perhaps this should come with a change of |
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... |
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. |
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. |
Given that we now have at least one authoring-only syntax extension, i.e. |
Hmmm, that's true - |
One use case I have run into for generics is having something resembling An alternative to avoid the extra complexity of the generic union would be something as simple as
|
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>!
} |
|
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!
}
|
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 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! |
@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. |
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 |
To add another use case, I've found myself making custom scalars when I would otherwise define a -- 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. |
Another use, also tied with pagination is when different interfaces need to be paginated. Suppose we have an interface Without generics, I cannot see a solution to be able to deduct that |
@mike-marcacci I'm not sure how interfaces implementing interfaces is enough to have a solution for the problem |
Although I agree, the problem is related. |
Oh I see. :) |
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 generic ConnectionEdge<T> {
node: T
cursor: String
}
generic Connection<T> {
edges: ConnectionEdge<T>
pageInfo: PageInfo
}
type Foo {
id: ID!
bars: Connection<Bar>
} And the 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. |
@akomm's recommendation is the style that I've gone with in my project. I currently implemented it with 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 |
Based on quick investigation I think could be a couple of approaches to resolving the problem with the type name
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: 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
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 SummaryFor 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. |
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
} |
@justinfagnani That seems like a use case for interfaces or unions. |
I don't think it's a tech problem for not implementing this for 5 years. |
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 ErrorsIn 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 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. |
GraphQL libraries in different languages usually provide at least two methods of defining the 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 |
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 us it's important that the use-site of the generic type specifies if 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. |
Regarding the templating approach it quickly gets out of hand when Lists and NonNull types are involved.
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" } } } |
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 |
Any progress or developments on this? |
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 |
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. |
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. |
Going schema DSL in the 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 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. |
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. |
Is there a specific blocker to making this happen? |
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,
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 The next question is,
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.
I think it does fulfill them with two possible solutions on the table,
Discussing the 2nd solution further,
Such query is resolvable in the code implementation by mapping the type based on the 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). |
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
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 |
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 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 |
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:
I'd like to start discussion about being able to do something similar for user-defined structures:
The overall goal is to reduce the amount of boilerplate in creating schemas with repetitive structures.
The text was updated successfully, but these errors were encountered: