-
-
Notifications
You must be signed in to change notification settings - Fork 29
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
Strongly typed unions #283
Comments
Hi, thanks for your interest. In your case, is import { onUnion, mutation, types } from 'typed-graphqlify'
mutation('confirmEmail', {
confirmEmail: {
...onUnion({
__typename: types.constant('EmailConfirmation'),
message: types.string,
}),
...onUnion({
__typename: types.constant('NotFoundError'),
message: types.string,
}),
},
}); Details ➡️ https://github.com/acro5piano/typed-graphqlify#inline-fragment By the way, I think it is better idea to use GraphQL's standard way to handle errors. You should return error objects defined in RFC: |
It's enough to get it to work, but I sacrifice a lot of type safety benefits:
This means that the caller will have to know which subset of fields belong to which
Thanks for the suggestion. It seems like the community is split on this. I ultimately decided to follow @leebyron's advice in graphql/graphql-spec#391 (comment):
RE: https://www.apollographql.com/docs/apollo-server/data/errors/#throwing-errors, I'm not using Apollo, so the behavior described in the link is not immediately available to me. Originally, I've been encoding errors in the top-level By including error types in the domain, I can rely on the compiler to stop me from using invalid fields for a given These articles also influenced my decision when designing the API: |
Sorry my example was wrong. It should be something like this: const m = mutation('confirmEmail', {
confirmEmail: {
...onUnion({
EmailConfirmation: {
__typename: types.constant('EmailConfirmation'),
message: types.string,
confirmedAt: types.string,
},
NotFoundError: {
__typename: types.constant('NotFoundError'),
message: types.string,
},
}),
},
}) And it actually becomes intersection type as you stated. However, type guard works to some extent like this: I don't want to add another union helper and interface, so if you improve the current type definition instead of creating a new one, I'm happy to merge it!
Oh I didn't notice that. Thanks for sharing! |
By the way, Relay also recommends your choice which uses Error type as a model. I didn't notice that again. https://relay.dev/docs/guided-tour/rendering/error-states/#accessing-errors-in-graphql-responses |
Thanks for that resource. I've migrated one of my codebases to use that pattern, and having the typed errors is so far a better developer experience. The main tradeoff is that my server code is more complex, but it ultimately forced me to surface implicit behavior in tests. I spent a few days making the union typing perfect before attempting a PR, but I was unsuccessful. I ran into an issue regarding weak types. When specifying fields for any type of GraphQL query, it only makes sense to make the specification a deep partial. Otherwise, we lose one of the main benefits of GraphQL. This is also a hard requirement for two-way relationships. For example: type UserOutput = User | NotFoundError
type User = {
__typename: 'User';
username: string;
post: Post[];
};
type Post = {
__typename: 'Post';
title: string;
body: string;
author: User;
reviewer: User;
}; We must be able to specify a deep partial, or there will be an infinite cycle selecting fields from However, using deep partials in my type definitions made the types weak. This allowed unknown keys to be specified, which was undesirable: types.union<UserOutput>()({
User: {
username: types.string(),
body: types.string(), // GOOD: the compiler does complain about this
foobar: types.string(), // BAD: the compiler didn't complain about this
},
Post: {
title: types.string(),
},
}); I got around this by requiring the types.union<UserOutput>()({
User: {
__typename: types.constant('User'),
username: types.string(),
body: types.string(), // GOOD: the compiler does complain about this
foobar: types.string(), // GOOD: the compiler does complain about this
},
Post: {
__typename: types.constant('Foobar'), // GOOD: the compiler does complain about this
title: types.string(),
},
}); I wanted to see if I can get around the stutter issue, but I haven't been successful. The reason why I chose an API of an object of What are your thoughts on the last example? Is this an acceptable API? I think I could make it backwards compatible by defaulting the union parameter to edit: I intend for this to be implemented in the existing |
A happy new year from Japan 🎉
Any changes that keeps the current API and improves type safety are welcome! |
When using
{ onUnion } from src/types.ts
, I immediately found that it was error-prone and not inherently safe against schema changes. Instead of using that, I created a strongly typedunion
type that wrapsonUnion
. I would like to share and get your thoughts on adding it as a static method on{ types } from src/types.ts
which will supplement{ onUnion } from src/types.ts
.An example usage is:
By leveraging the
__typename
field, we have a discriminated union that allows union queries to be strongly typed:It also requires all members of the union to be included, which prevents developers from forgetting to include a
__typename
from the union. In this example, I deleted theNotFoundError
from the union selection object, but the compiler complains, which is the desired result. This enforces the invariant that all members of a union are required in the query:What are your thoughts?
The text was updated successfully, but these errors were encountered: