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

Modeling certain tagged unions in GraphQL #489

Closed
dylanscott opened this issue Aug 2, 2018 · 7 comments
Closed

Modeling certain tagged unions in GraphQL #489

dylanscott opened this issue Aug 2, 2018 · 7 comments

Comments

@dylanscott
Copy link

Apologies if this is the wrong place to ask this kind of question, but I was hoping to get some advice on how to model a specific scenario in my GraphQL schema. Let's say in my application I'm modeling a user's payment method as follows (this scenario is hypothetical, all that matters is that some of the members of the tagged union have no fields other than the discriminator):

type PaymentMethod = Cash | PayPal | CreditCard;

interface Cash {
  type: "cash";
}

interface PayPal {
  type: "paypal";
  email: string;
}

interface CreditCard {
  type: "credit";
  cardNumber: string;
}

What would be the right way to model this in GraphQL? I would initially try:

union PaymentMethod = Cash | PayPal | CreditCard
type Cash {}
type PayPal {
  email: String!
}
type CreditCard {
  cardNumber: String!
}

But this violates the spec, because object types are required to have one or more fields. I suppose I could throw a type field on there, but it's redundant - I would actually want to query __typename. Is there a better way to model this in GraphQL?

@jlouis
Copy link

jlouis commented Aug 3, 2018

GraphQL current have no support for sum types, so you are more or less forced to simulate them somehow. If you are lucky, your Cash, CC and Paypal all have identity so they contain an id field or something such. But if you have no such value, you quickly get into trouble.

In a sense, your "Cash" is a phantom type, since it cannot have any inhabitant in the system. I'm not sure how this would be useful to a client. So perhaps you can explain the need from the clients perspective to model the data like this (either by means of this example, or a new one).

@smolinari

This comment has been minimized.

@dylanscott
Copy link
Author

I'm open to modeling things in a different way in the schema given the lack of support for sum types, I'm just not sure what to do in cases like this where an element of the sum type carries no additional data other than their type. If the cash example is too contrived, consider modeling search filters:

// maybe these are search filters for an apartment search service
// represented here in TypeScript (might be valid Flow too?)

type SearchFilter = PriceFilter | NeighborhoodFilter | HasImageFilter;

interface PriceFilter {
  type: "price";
  minPrice?: number;
  maxPrice?: number;
}

interface NeighborhoodFilter {
  type: "neighborhood";
  neighborhood: string;
}

interface HasImageFilter {
  type: "has_image";
  // there's no additional data here, the type says it all
}

I'm mostly trying to understand if there are best practices around modeling situations like this in GraphQL. The search filter case is maybe a better example because you probably would want to specify these types as an input to a query, and input types don't even support unions (#488). The only way I could imagine modeling that case as inputs would be:

enum SearchFilterType {
  PRICE
  NEIGHBORHOOD
  HAS_IMAGE
}

input SearchFilter {
  type: SearchFilterType!

  # only set if type == PRICE
  minPrice: Number
  maxPrice: Number

  # only set if type == NEIGHBORHOOD
  neighborhood: String
}

Which honestly feels pretty bad.

@leebyron
Copy link
Collaborator

leebyron commented Oct 2, 2018

I think providing an enum is a reasonable step, or just provide some useless field to fulfill the validation requirements.

If you think it's a common enough case, you could propose removing the rule that object types require at least one field - though that often protects against accidental cases where fields are forgotten.

For the search filter, that's been discussed as input unions elsewhere. I think having literal string types to allow for sum-types for input unions is probably a better implementation than other proposals so far.

If you're willing, I encourage you to create RFCs for these ideas if you can see them through.

@leebyron leebyron closed this as completed Oct 2, 2018
@slorber
Copy link

slorber commented Sep 16, 2019

Hi,

@leebyron wonder if there is an issue somewhere about using scalar instances as types directly?

For example, using string literals directly:

union PaymentMethod = Cash | PayPal | CreditCard
type Cash {
  type: "cash"
}
type PayPal {
  type: "paypal"
  email: String!
}
type CreditCard {
  type: "creditcard"
  cardNumber: String!
}

What I'm looking for is mostly to have codegen tools being able to generate correct TS typings, using a discriminent, so that doing if (data.type === "creditcard") enables TS code to know that the cardNumber property is accessible.

@benjie
Copy link
Member

benjie commented Sep 16, 2019

@slorber That's normally solved by using the __typename introspection field, code generators such as graphql-code-generator can enable type narrowing via if (data.__typename === "CreditCard").

@slorber
Copy link

slorber commented Sep 16, 2019

ah yeah thanks ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants