-
GraphQL servers do not allow you to pass in excess properties on input types. Doing so results in a server side error. For example, say we have the following GraphQL schema: input Input {
foo: String!
bar: Int!
}
type Mutation {
send(input: Input!): Boolean
} Then the client sends the following data to the { "foo": "hello", "bar": 123, "excess": "excess" } This would lead to the following server side error:
Now, what's the big deal you may wonder, just pass in the correct values? While that is true, I would argue it goes against the purpose of type safety to have to remember to do things right and not trust the compiler or type checker to do its job. And unfortunately, due to the structural nature of typescript, it's actually quite simple to end up with a runtime error that typescript itself cannot detect: Here is an example react component in typescript that contains no type errors, but one of the mutation calls will end up with the runtime server error mentioned above: import { useMutation } from "urql";
import { graphql } from "./gql-tada";
export function App() {
const [mutationResult, sendStuff] = useMutation(mutateGQL);
const exactInput = { foo: "hello", bar: 123 };
const excessInput = { foo: "hello", bar: 123, excess: "excess" };
// This is structurally equal to the input type, but contains excess props, which is a runtime error
return (
<div>
<button onClick={() => sendStuff({ input: exactInput })}>
Send input to graphql server (with exact input)
</button>
<br />
<button onClick={() => sendStuff({ input: excessInput })}>
Send input to graphql server (with excess props on input)
</button>
</div>
);
}
const mutateGQL = graphql(`
mutation Mutate($input: Input!) {
send(input: $input)
}
`); This is a behavior that cannot be disabled in the standard node graphql implementation. And since I wanted to start this discussion and hear what others think of how one should deal with this problem. I've also made a minimal demo repo with My current solutionWhat I'm currently doing is using a custom urql exchange that i wrote that omits excess props automatically in the client. It works great, but it comes at the cost that you'll need the schema metadata embedded in your bundle. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
There's probably a way to co-opt The problem here is that there always needs to be an exact object literal check where that object is defined, since TypeScript doesn't currently have In certain conditions, TypeScript will however trigger error 2353 (“object literals may only specify known properties”). This doesn't always happen however. I think the best way to ensure that is to use const exactInput = graphql.scalar('Input', { foo: "hello", bar: 123 }); There's still only certain conditions in which TypeScript can warn you about excess properties, however, I'd say that an API error is in these cases still preferable sometimes |
Beta Was this translation helpful? Give feedback.
-
Another idea, there's a chance that you could make a generic import type { DocumentDecoration } from 'gql.tada';
function variables<Variables extends {}, T extends {}>(
_query: DocumentDecoration<any, Variables>,
vars: T & Exact<Variables, T>
): Variables {
return vars as Variables;
}
type Exact<Shape, T> = {
[Key in keyof Shape | keyof T]: Key extends keyof Shape
? Key extends keyof T
? Shape[Key] extends any[]
? T[Key] extends any[]
? Exact<Shape[Key][number], T[Key][number]>
: Shape[Key]
: Shape[Key] extends readonly any[]
? T[Key] extends readonly any[]
? Exact<Shape[Key][number], T[Key][number]>
: Shape[Key]
: Shape[Key] extends { [key: string | number]: any }
? T[Key] extends { [key: string | number]: any }
? Exact<Shape[Key], T[Key]>
: Shape[Key]
: Shape[Key]
: Shape[Key]
: never
}; Just to be clear, I haven't tested this yet, but it's basically just a way of comparing the two generic shapes, and making sure that excess properties are marked as |
Beta Was this translation helpful? Give feedback.
The extended answer here to narrow it down is that we don't and won't generate any runtime code: #263 (comment)
That's not a specific goal of the project.
This is basically a problem with all API inputs, and you could also pre-filter arguments on the server-side and instead warn about this if you're concerned about accidentally failing your requests due to smaller changes in the state.
However, I'd say, given the TypeScript constraint here that exact record types aren't possible, it's not too much trouble to map over inputs just before you send them off as a mutation. IMHO, from a perspective of writing out a couple of properties it isn't too bad.
If this does get more you could consider …