-
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
Consolidate Input Union discussions #627
Comments
Thanks for all your hard work in consolidating this @binaryseed, @benjie, et al! To whom can I send virtual beers so this gets on the working group's agenda? 🍻 |
Thanks for the support and offer @dendrochronology; we discuss this frequently at working groups so it’s definitely already on our agenda :) We’re aiming to have the RFC doc updated by the next WG to enable participants to have a more informed discussion. It seems that there is no perfect solution (at least we haven’t found one yet!) so now it’s time to weigh up the different proposals and find out which trade-offs we can/can’t live with, which ones are the easiest to implement cross-platform, and which features we need/want/don’t care about. |
@leebyron Would you mind taking a look at this?! |
@binaryseed Terribly sorry for the delay. |
I refer to this answer, I do not want to continue in the closed topic. @benjie I have more and more the feeling, that the current graphql enumeration should have been more powerful in terms of its allowed values and work similar to how the proposed The advantage is clear, you do not need an artificial discriminator. Neither on the input, nor output side. The It might be considered something long-term, maybe for graphql v2, as an enum replacement. Short/mid-termn |
@akomm Thanks for the feedback :) One issue that interface Product {
name: String!
priceInCents: Int!
}
type Clothing implements Product {
# ...
size: String
}
type Food implements Product {
# ...
calories: Int
}
type Cleaning implements Product { ... }
type Query {
search(query: String): [Product]
} you might issue this request: {
search(query: "cream") {
__typename
... on Product { name priceInCents }
... on Clothing { size }
}
} This allows you to display a common search interface for all your products displaying name and price; but for certain "supported" products you could enhance the display with extra metadata - e.g. size for clothing or calories for food. I'm not sure how you'd achieve this in the I think |
@benjie is it really simpler in a practical situation? I think not only the query should be considered, but also working with the result on the client. The advantage in your example is that you get the data as a flat collection. But you also have to discriminate by a string value on if (result.clothing) {
return <Clothing data={result.clothing} />;
}
if (result.cleaning) {
return <Cleaning data={result.cleaning} />;
} vs. switch (result.__typename) {
case 'Clothing':
return <Clothing data={result} />;
case 'Cleaning':
return <Cleaning data={result} />;
} For this to properly type-check with typescript, the typing extracted from introspection must provide the Comparison type emission for both interface Product {}
interface Clothing extends Product {}
interface Food extends Product {}
interface Cleaning extends Product {}
// This is needed for oneField
type Result =
| {clothing: Clothing}
| {cleaning: Cleaning}
| {food: Food}
// vs __typename that uses string literal union based on type names
type Result =
| ({__typename: 'Clothing'} & Clothing)
| ({__typename: 'Cleaning'} & Cleaning)
| ({__typename: 'Food'} & Food) Actually, with strict setting the type Result =
| ({clothing: Clothing} & {cleaning?: undefined, food?: undefined})
| ({cleaning: Cleaning} & {clothing?: undefined, food?: undefined})
| ({food: Food} & {cleaning?: undefined, clothing?: undefined}) With ever growing list... Or alternative, make assertions, which is very ugly and can be done different ways, I do not even try it here... :) As for the query. I would like to compare how you could do both with more data fetched to make clear, where the difference is: query ProductSearchQuery {
search {
clothing {
...ProductData
size
}
cleaning {
...ProductData
}
food {
...ProductData
calories
}
}
}
fragment ProductData on Product {
name
priceIncents
} vs query ProductSearchQuery {
search {
__typename
... on Product {
name
priceIncents
}
... on Clothing {
size
}
... on Food {
calories
}
}
} But this is just the js/ts side of the client. There are obviously other languages to be considered where the picture might be a different one... |
You raise an interesting point: should Note that the Product interface could be implemented by hundreds of types, and you may only have special treatment for a few of them (Clothing, Food). With my code you can add more types to the Product interface later and without having to update your client or change the query your application will continue to work because it can rely on |
The issue with the typing in TS with fully strict setting is, that you can not check for the variants. The keys can only be checked against when they are explicitly defined, even if the only possible value is null or undefined. interface Product {}
interface Clothing extends Product {}
interface Food extends Product {}
interface Cleaning extends Product {}
// This is needed for oneField
type Result =
| {clothing: Clothing}
| {cleaning: Cleaning}
| {food: Food}
function render(result: Result) {
// TS2339: Property 'clothing' does not exist on type 'Result'.
// Property 'clothing' does not exist on type '{ cleaning: Cleaning; }'.
if (result.clothing) {}
} You would have to make assertions/type-guards for every product type. |
@akomm How about: type Result =
| {food?: null, cleaning?: null, clothing: Clothing}
| {food?: null, cleaning: Cleaning, clothing?: null}
| {food: Food, cleaning?: null, clothing?: null} It's a bit verbose but I guess that wouldn't be a problem if it's generated, right? |
@Janpot which is basically what I have posted here, refined further. Even if it is generated, it might be an issue because TS will not give you a readable error. Imagine having 100 or more different products with different properties. Also not sure what implication it would have on TS performance if the type gets ridiculous large. At this point, I do not want to get away from the main topic, so to cut in in general: I think that is should not be the desired outcome to have to generate such types ;) |
You could do it like: type Result_void = { food?: null, cleaning?: null, clothing?: null };
type Result =
| { food: Food } & Omit<Result_void, 'food'>
| { cleaning: Cleaning } & Omit<Result_void, 'cleaning'>
| { clothing: Clothing } & Omit<Result_void, 'clothing'>; but yeah, it's not super elegant. You could also use auto-generated type guards, but that's not great either. It's a shame TypeScript doesn't seem to have better support for this pattern, because in vanilla JS it'd be absolutely fine. |
Looks like type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>>
& {
[K in Keys]-?:
Required<Pick<T, K>>
& Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys];
type Result = RequireOnlyOne<{ food: Food, cleaning: Cleaning, clothing: Clothing }> |
[Action from GraphQL Spec WG]: I volunteer to champion solution 5 (the |
@benjie Could you start a PR that summarizes the introspection changes needed for solution 5 into the RFC doc? |
Sure; I’ll try and get that done in the next week or two. Update: submitted: #660 |
@binaryseed Would you be open to a PR that replaces all the inline links with reference-style links; e.g. | | [1](#-1-explicit-__typename-discriminator-field) | [2](#-2-explicit-configurable-discriminator-field) | [3](#-3-order-based-discrimination) | [4](#-4-structural-uniqueness) | [5](#-5-one-of-tagged-union) |
| -- | -- | -- | -- | -- | -- |
| [K](#-k-input-unions-should-be-expressed-efficiently-in-the-query-and-on-the-wire) | ❔ | ❔ | ❔ | ❔ | ❔ |
| [L](#-l-input-unions-should-be-performant-for-servers) | ❔ | ❔ | ❔ | ❔ | ❔ |
| [M](#-m-existing-sdl-parsers-are-backwards-compatible-with-sdl-additions) | ❔ | ❔ | ❔ | ❔ | ❔ |
| [N](#-n-existing-code-generated-tooling-is-backwards-compatible-with-introspection-additions) | ❔ | ❔ | ❔ | ❔ | ❔ | would become: | | [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
| -- | -- | -- | -- | -- | -- |
| [K][criteria-K] | ❔ | ❔ | ❔ | ❔ | ❔ |
| [L][criteria-L] | ❔ | ❔ | ❔ | ❔ | ❔ |
| [M][criteria-M] | ❔ | ❔ | ❔ | ❔ | ❔ |
| [N][criteria-N] | ❔ | ❔ | ❔ | ❔ | ❔ |
[criteria-K]: #-k-input-unions-should-be-expressed-efficiently-in-the-query-and-on-the-wire
[criteria-L]: #-l-input-unions-should-be-performant-for-servers
[criteria-M]: #-m-existing-sdl-parsers-are-backwards-compatible-with-sdl-additions
[criteria-N]: #-n-existing-code-generated-tooling-is-backwards-compatible-with-introspection-additions
[solution-1]: #-1-explicit-__typename-discriminator-field
etc (And all the links in the rest of the document would get cleaner too.) |
Would those be clickable links still? I don't care much what the markdown looks like for those, really what matters is that they help navigate around the document |
Yeah, they work exactly the same (the output HTML is identical) it just makes it easier to read/edit the markdown. |
then yes, sounds great! |
Hi guys! Or we can write inputunion in some other way. Spent so much time by searching an answer) |
Stopping in to share my thoughts on the RFC, as I just ran into this while writing up my owl gql schema. The reason I'm chiming in is that (1) I think my use-case is unique among those discussed so far and (2) it provides a use-case for criteria G ("Input unions may include other input unions"). In my case, I want to use unions as a kind of more-sophisticated ID. On my system, users are either uniquely identified by an integer ID, or by their username. Users also own repository resources, which can themselves by uniquely identified by an integer ID, or by a combination of user and repository name. To make things convenient on API users, I want to give them all of these options for unambiugously specifying a repository. Therefore, I have the following schema:
|
Closing since these discussions were consolidated in the RFC document! |
Can you link to the most current RFC document and the appropriate place for its discussion, @binaryseed? This issue was already supposed to consolodate several and now it's fractured again... |
The RFC is linked in the issue description, and discussions can happen through PRs to that document! |
For the casual reader, looks like the action is happening here #733 |
Looks like the currently-open discussion is here: #825 |
Over the years there have been many discussions around the topic of Input Unions in GraphQL. To help further the effort, we have begun building a single RFC document. The goal of this document is to consolidate all related ideas to help move the proposal forward.
To help this effort, I'd like @leebyron to consider commenting on & closing all related open Issues / PRs in favor of the main RFC document, which will contain & reference all of these previous discussions.
@leebyron @IvanGoncharov
The text was updated successfully, but these errors were encountered: