Why Uniform GraphQL
The motivation and philosophy behind Uniform GraphQL
tl;dr: Type-safety is concerned with compile time whereas GraphQL schemas are concerned with runtime. What we need is a uniform approach that is type-safe at compile time, while preserving a runtime type information that carries smoothly to GraphQL schemas. And that's what
uniform-graphql
is all about: Helping you build robust GraphQL schemas - and resolvers - by delegating all forms of type-safety to the compiler.
Motivation
GraphQL
apis usually fall under two schools of thought: schema-first
vs code-first
. Schema-first GraphQL
apis create the typedefs first - including all queries, mutations and subscriptions - and implement the corresponding resolvers after. Code-first apis on the other hand implement the resolvers first and have the typedefs derived from the code. Both approaches have their pros and cons. Uniform GraphQL
falls somewhere in the middle, but it's closer to the code-first camp.
The biggest issue with the currently available code-first approaches emerges during the schema-generation phase: a non-trivial mismatch between the implemented resolvers and the generated schema. Developers can't simply rely on the compile-time type system to make sure that their code will match the generated schema, so they have to resort to other means such as decorators and other runtime checks. But it doesn't have to be that way. As it turns out, this is a perfect job for the type system of TypeScript
.
End-to-End Type Safety
Uniform GraphQL
is built with compile time type-safety front and center, making it very hard for you to experience type errors at runtime. You will find a simple, streamlined approach that will guide you end-to-end while building your GraphQL
api. Common examples include:
- Creating primitive
GraphQL
types - Composing said
GraphQL
types to create more complex types - Implementing query & mutation resolvers that work on said types
- Implementing field resolvers on object types
Non-Null First
In GraphQL
, types are null-first
, which means they are nullable unless explicitly wrapped with a GraphQLNonNull
type. In TypeScript
on the other hand, types are non-null-first
: non-nullable by default unless they are explicitly made nullable. This tension is something that few code-first approaches acknowledge and solve for, which results in schema-code mismatches and general developer pain.
/**
* In GraphQL, types are nullable unless explicitly made non-nullable.
*/
import {
GraphQLID,
GraphQLNonNull,
GraphQLObjectType,
GraphQLString,
} from 'graphql';
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
fullName: { type: GraphQLString },
},
});
When it comes to nullability, Uniform GraphQL
sides with TypeScript
. We are a code-first library and we want to play nicely with our programming language. This is why everything is non-nullable unless they are explicitly made nullable.
/**
* In Uniform GraphQL, types are nullable unless
* explicitly made non-nullable.
*/
import { t } from '@whatsgood/uniform-graphql';
const User = t.object({
name: 'User',
fields: {
id: t.id,
fullName: t.string.nullable,
},
});
After all is said and done, we still need to create a GraphQL
schema. Below is the runtime typedef
of Uniform GraphQL
. The key point is that both Uniform GraphQL
and the reference graphql
library eventually yield the same typedef
. The difference however, is that Uniform GraphQL
will also keep your actual resolver implementations compile-time type safe.
# Both codes result with the same typedef:
type User {
id: ID!
fullName: String
}
Composability
One of GraphQL
's main benefits is the reusability and composability of types. You can create an enum
type, which you use in an object
type, which you use in a list
type, which you use in an interface
type, which you use in another object
type, which you use in a union
type and so on.
In Uniform GraphQL
, you are able to infinitely compose and reuse your types. This includes self referential and mutually recursive types, while always adhering to our core principle of end-to-end type safety. Keep on reading to see how we handle such use cases.
# Self referential type
type User {
id: ID!
friends: [User]! # type A refers to itself
}
# Mutually recursive types
type Person {
id: ID!
pets: [Animal]! # type A refers to type B
}
type Animal {
id: ID!
owner: Person! # type B refers to type A
}
Uniform Type System
No need to maintain two separate type systems for GraphQL
and TypeScript
while trying to keep them in sync. Once you create your uniform
types, all will be taken care of. You will never need to manually type out function parameter types or return types. Everything is inferred from your uniform
types; all you need to do is to fill in the blanks.
Example 1: The compiler is complaining because the resolve
function is incorrectly implemented. When we ask for hints on the membership
field, we are shown that we need to return one of the listed type literals.
Example 2: When we hover over args.id
, we see that it's a union type between string
and number
. All this type information comes directly through the library. While developing our GraphQL api, we don't need to manually write any TypeScript
types for our resolvers. This inclues the resolver function arguments and the return type.