11/9/2021

tsgql

My novel solution to the Typescript <-> GraphQL code generation problem


With GraphQL you have the bookkeping problem of managing both the schema's representation of your data, and your code's representation of data. The solution is to choose one as the single source of truth, and use it to generate the other.

The standard choice is the GraphQL schema, and then generating types for your desired language from it. There is plenty of tooling to facilitate this method. This is at first glance a robust solution, but ultimately it is a facade, because the GraphQL schema language itself lends to this type of problem. Allow me to explain.

For example, suppose I have user with fields id, name, email, age. Now I want to create an operation to update a user, I don't want to allow updates to a user's ID, and I would like to make the rest of the fields optional if I for example only want to update a single field.

Writing the GraphQL schema would look something like this:

type User { id: Int! name: String! email: String! age: Int! } """ We don't want to update a User's ID, and we make the other fields optional so we can use any combination of them. """ input UpdateUserInput { name: String email: String age: Int } type Mutation { updateUser(id: Int!, input: UpdateUserInput): User }

But as you can see there is some redundancy in that I need to copy the fields from User. And what happens if I add, remove, or change a field? It's very easy to forget to propagate this alteration to the Input type, and imagine how the problem exacerbates when we have a large number of types and fields.

The end result is that we have circled back to the original bookkeeping problem we intended to solve. This is because GraphQL lacks a way to make operations/selections on the sets that types represent, this is the exact class of operations Typescript is uniquely equipped to do.

With tsgql this bookkeeping is finally eradicated. This is because we use Typescript's type system, which allows us to alias and reference other types, as well as expand or reduce our type set. The GraphQL schema above can be written with tsgl in Typescript like so:

export type User { id: number name: string email: string age: number } export type Mutation = { updateUser: (args: { id: number, input: Partial<Omit<User, 'id'>>}) => Promise<User>; }

tsgql will generate the GraphQL schema depicted previously, using the just the Typescript types you see directly above. As long as a type is marked with export, it will be transpiled into the appropriate GraphQL schema type.

With this approach you can use any Typescript type feature at your disposal, including importing types from modules, creating type aliases, conditional types, recursive types, and any of Typescript's type utilities.

At modfy, our specific use case was using DB types generated from our Prisma schema.

// Import Prisma generated types import { User } from '@prisma/client' // Tell tsgql to generate the type export { User } // Declare constant fields for User, using `Pick<T, K>` so we // get errors if we select fields that don't exist on the type. type ConstantUserFields = Pick<User, 'id' | 'email' | 'createdAt'> // Declare a type alias for the constant fields all made optional type UpdateUserFields = Partial<Omit<User, keyof ConstantUserFields>> type Mutation = { updateUser: (args: { id: number, input: UpdateUserFields}) => Promise<User>; }

Got any questions? Did I say something completely bogus and wrong? Hit me up on Twitter.