@graphql-ts/schema
is a thin wrapper around
GraphQL.js providing type-safety for
constructing GraphQL Schemas while avoiding type-generation, declaration
merging
and
decorators.
import { gWithContext } from "@graphql-ts/schema";import { GraphQLSchema, graphql } from "graphql";type Context = {loadPerson: (id: string) => Person | undefined;loadFriends: (id: string) => Person[];};const g = gWithContext<Context>();type g<T> = gWithContext.infer<T>;type Person = {id: string;name: string;};const Person: g<typeof g.object<Person>> = g.object<Person>()({name: "Person",fields: () => ({id: g.field({ type: g.nonNull(g.ID) }),name: g.field({ type: g.nonNull(g.String) }),friends: g.field({type: g.list(g.nonNull(Person)),resolve(source, _, context) {return context.loadFriends(source.id);},}),}),});const Query = g.object()({name: "Query",fields: {person: g.field({type: Person,args: {id: g.arg({ type: g.nonNull(g.ID) }),},resolve(_, args, context) {return context.loadPerson(args.id);},}),},});const schema = new GraphQLSchema({query: Query,});{const people = new Map<string, Person>([["1", { id: "1", name: "Alice" }],["2", { id: "2", name: "Bob" }],]);const friends = new Map<string, string[]>([["1", ["2"]],["2", ["1"]],]);const contextValue: Context = {loadPerson: (id) => people.get(id),loadFriends: (id) => {return (friends.get(id) ?? []).map((id) => people.get(id)).filter((person) => person !== undefined) as Person[];},};graphql({source: `query {person(id: "1") {idnamefriends {idname}}}`,schema,contextValue,}).then((result) => {console.log(result);});}
The gWithContext
function accepts a Context
type parameter which binds
the returned functions so they can be used to compose GraphQL types into a
GraphQL schema.
A simple schema with only a query type looks like this:
import { gWithContext } from "@graphql-ts/schema";import { GraphQLSchema, graphql } from "graphql";type Context = {};const g = gWithContext<Context>();type g<T> = gWithContext.infer<T>;const Query = g.object()({name: "Query",fields: {hello: g.field({type: g.String,resolve() {return "Hello!";},}),},});const schema = new GraphQLSchema({query: Query,});graphql({source: `query {hello}`,schema,}).then((result) => {console.log(result);});
You can use pass the schema
to ApolloServer
and other GraphQL servers.
You can also create a more advanced schema with other object types, circular
types, args, and mutations. See GWithContext for what the other
functions on g
do.
import { gWithContext } from "@graphql-ts/schema";import { GraphQLSchema, graphql } from "graphql";import { deepEqual } from "node:assert";type Context = {todos: Map<string, TodoItem>;};const g = gWithContext<Context>();type g<T> = gWithContext.infer<T>;type TodoItem = {id: string;title: string;relatedTodos: string[];};const Todo: g<typeof g.object<TodoItem>> = g.object<TodoItem>()({name: "Todo",fields: () => ({id: g.field({ type: g.nonNull(g.ID) }),title: g.field({ type: g.nonNull(g.String) }),relatedTodos: g.field({type: g.list(Todo),resolve(source, _args, context) {return source.relatedTodos.map((id) => context.todos.get(id)).filter((todo) => todo !== undefined);},}),}),});const Query = g.object()({name: "Query",fields: {todos: g.field({type: g.list(Todo),resolve(_source, _args, context) {return context.todos.values();},}),},});const Mutation = g.object()({name: "Mutation",fields: {createTodo: g.field({args: {title: g.arg({ type: g.nonNull(g.String) }),relatedTodos: g.arg({type: g.nonNull(g.list(g.nonNull(g.ID))),defaultValue: [],}),},type: Todo,resolve(_source, { title, relatedTodos }, context) {const todo = { title, relatedTodos, id: crypto.randomUUID() };context.todos.set(todo.id, todo);return todo;},}),},});const schema = new GraphQLSchema({query: Query,mutation: Mutation,});(async () => {const contextValue: Context = { todos: new Map() };{const result = await graphql({source: `query {todos {title}}`,schema,contextValue,});deepEqual(result, { data: { todos: [] } });}{const result = await graphql({source: `mutation {createTodo(title: "Try graphql-ts") {title}}`,schema,contextValue,});deepEqual(result, {data: { createTodo: { title: "Try graphql-ts" } },});}{const result = await graphql({source: `query {todos {title}}`,schema,contextValue,});deepEqual(result, {data: { todos: [{ title: "Try graphql-ts" }] },});}})();
The gWithContext.infer
type is useful particularly when defining circular
types to resolve errors from TypeScript because of the circularity.
We recommend aliasing gWithContext.infer
to your g
like this to make it
easier to use:
import { gWithContext } from "@graphql-ts/schema";type Context = {};const g = gWithContext<Context>();type g<T> = gWithContext.infer<T>;type PersonSource = { name: string; friends: PersonSource[] };const Person: g<typeof g.object<PersonSource>> =g.object<PersonSource>()({name: "Person",fields: () => ({name: g.field({ type: g.String }),friends: g.field({ type: g.list(Person) }),}),});
Creates a GraphQL object type.
Note this is an output type, if you want an input object, use
g.inputObject
.
When calling g.object
, you must provide a type parameter that is the
source of the object type. The source is what you receive as the first
argument of resolvers on this type and what you must return from resolvers
of fields that return this type.
const Person = g.object<{ name: string }>()({name: "Person",fields: {name: g.field({ type: g.String }),},});// ==graphql`type Person {name: String}`;
To do anything other than just return a field from the source type, you need to provide a resolver.
Note: TypeScript will force you to provide a resolve function if the field in the source type and the GraphQL field don't match
const Person = g.object<{ name: string }>()({name: "Person",fields: {name: g.field({ type: g.String }),excitedName: g.field({type: g.String,resolve(source, args, context, info) {return `${source.name}!`;},}),},});
GraphQL types will often contain references to themselves and to make
TypeScript allow that, you need have an explicit type annotation of
g<typeof g.object<Source>>
along with making fields
a function that
returns the object.
type PersonSource = { name: string; friends: PersonSource[] };const Person: g<typeof g.object<PersonSource>> =g.object<PersonSource>()({name: "Person",fields: () => ({name: g.field({ type: g.String }),friends: g.field({ type: g.list(Person) }),}),});
Create a GraphQL union type.
A union type represents an object that could be one of a list of types. Note it is similar to an interface type except that a union doesn't imply having a common set of fields among the member types.
const A = g.object<{ __typename: "A" }>()({name: "A",fields: {something: g.field({ type: g.String }),},});const B = g.object<{ __typename: "B" }>()({name: "B",fields: {differentThing: g.field({ type: g.String }),},});const AOrB = g.union({name: "AOrB",types: [A, B],});
Creates a GraphQL field.
These will generally be passed directly to the fields
object in a
g.object
call.
const Something = g.object<{ thing: string }>()({name: "Something",fields: {thing: g.field({ type: g.String }),},});
A helper to declare fields while providing the source type a single time rather than in every resolver.
const nodeFields = g.fields<{ id: string }>()({id: g.field({ type: g.ID }),relatedIds: g.field({type: g.list(g.ID),resolve(source) {return loadRelatedIds(source.id);},}),otherRelatedIds: g.field({type: g.list(g.ID),resolve(source) {return loadOtherRelatedIds(source.id);},}),});const Person = g.object<{id: string;name: string;}>()({name: "Person",fields: {...nodeFields,name: g.field({ type: g.String }),},});
Creates a GraphQL interface field.
These will generally be passed directly to the fields
object in a
call. Interfaces fields are similar to
regular fields except that they don't define how the field
is resolved.
const Entity = g.interface()({name: "Entity",fields: {name: g.interfaceField({ type: g.String }),},});
Note that regular fields are assignable to interface fields but the opposite is not true. This means that you can use a regular field in an interface type.
Creates a GraphQL interface type that can be implemented by other GraphQL object and interface types.
const Entity = g.interface()({name: "Entity",fields: {name: g.interfaceField({ type: g.String }),},});type PersonSource = { __typename: "Person"; name: string };const Person = g.object<PersonSource>()({name: "Person",interfaces: [Entity],fields: {name: g.field({ type: g.String }),},});type OrganisationSource = {__typename: "Organisation";name: string;};const Organisation = g.object<OrganisationSource>()({name: "Organisation",interfaces: [Entity],fields: {name: g.field({ type: g.String }),},});
When using GraphQL interface and union types, there needs to a way to
determine which concrete object type has been returned from a resolver.
With graphql-js
and @graphql-ts/schema
, this is done with isTypeOf
on
object types and resolveType
on interface and union types. Note
@graphql-ts/schema
does not aim to strictly type the implementation of
resolveType
and isTypeOf
. If you don't provide resolveType
or
isTypeOf
, a __typename
property on the source type will be used, if
that fails, an error will be thrown at runtime.
You might have noticed that g.interfaceField
was used instead of
g.field
for the fields on the interfaces. This is because interfaces
aren't defining implementation of fields which means that fields on an
interface don't need define resolvers.
Even though interfaces don't contain field implementations, you may still
want to share field implementations between interface implementations. You
can use g.fields
to do that. See g.fields
for more information about
why you should use g.fields
instead of just defining an object the fields
and spreading that.
const nodeFields = g.fields<{ id: string }>({id: g.field({ type: g.ID }),});const Node = g.field({name: "Node",fields: nodeFields,});const Person = g.object<{__typename: "Person";id: string;name: string;}>()({name: "Person",interfaces: [Node],fields: {...nodeFields,name: g.field({ type: g.String }),},});
A shorthand to easily create enum values to pass to .
If you need to set a description
or deprecationReason
for an enum
variant, you should pass values directly to g.enum
without using
g.enumValues
.
const MyEnum = g.enum({name: "MyEnum",values: g.enumValues(["a", "b"]),});
const values = g.enumValues(["a", "b"]);assertDeepEqual(values, {a: { value: "a" },b: { value: "b" },});
Creates an enum type with a number of enum values.
const MyEnum = g.enum({name: "MyEnum",values: g.enumValues(["a", "b"]),});// ==graphql`enum MyEnum {ab}`;
const MyEnum = g.enum({name: "MyEnum",description: "My enum does things",values: {something: {description: "something something",value: "something",},thing: {description: "thing thing",deprecationReason: "something should be used instead of thing",value: "thing",},},});// ==graphql`"""My enum does things"""enum MyEnum {"""something something"""something"""thing thing"""thing@ — \deprecated(reason: "something should be used instead of thing")}`;)
Creates a GraphQL argument.
Args can can be used as arguments on output fields:
g.field({type: g.String,args: {something: g.arg({ type: g.String }),},resolve(source, { something }) {return something || somethingElse;},});// ==graphql`(something: String): String`;
Or as fields on input objects:
const Something = g.inputObject({name: "Something",fields: {something: g.arg({ type: g.String }),},});// ==graphql`input Something {something: String}`;
Creates an input object type
const Something = g.inputObject({name: "Something",fields: {something: g.arg({ type: g.String }),},});// ==graphql`input Something {something: String}`;
Circular input objects require explicitly specifying the fields on the object in the type because of TypeScript's limits with circularity.
import { GInputObjectType } from "@graphql-ts/schema";type SomethingInputType = GInputObjectType<{something: g<typeof g.arg<SomethingInputType>>;}>;const Something: SomethingInputType = g.inputObject({name: "Something",fields: () => ({something: g.arg({ type: Something }),}),});
You can specify all of your non-circular fields outside of the fields
object and then use typeof
to get the type to avoid writing the
non-circular fields as types again.
import { GInputObjectType } from "@graphql-ts/schema";const nonCircularFields = {thing: g.arg({ type: g.String }),};type SomethingInputType = GInputObjectType<typeof nonCircularFields & {something: g<typeof g.arg<SomethingInputType>>;}>;const Something: SomethingInputType = g.inputObject({name: "Something",fields: () => ({...nonCircularFields,something: g.arg({ type: Something }),}),});
Wraps any GraphQL type in a list type.
const stringListType = g.list(g.String);// ==graphql`[String]`;
When used as an input type, you will recieve an array of the inner type.
g.field({type: g.String,args: { thing: g.arg({ type: g.list(g.String) }) },resolve(source, { thing }) {const theThing: undefined | null | Array<string | null> = thing;return "";},});
When used as an output type, you can return an iterable of the inner type
that also matches typeof val === 'object'
so for example, you'll probably
return an Array most of the time but you could also return a Set you
couldn't return a string though, even though a string is an iterable, it
doesn't match typeof val === 'object'
.
g.field({type: g.list(g.String),resolve() {return [""];},});
g.field({type: g.list(g.String),resolve() {return new Set([""]);},});
g.field({type: g.list(g.String),resolve() {// this will not be allowedreturn "some things";},});
Wraps a nullable type with a non-nullable type.
Types in GraphQL are always nullable by default so if you want to enforce that a type must always be there, you can use the non-null type.
const nonNullableString = g.nonNull(g.String);// ==graphql`String!`;
When using a non-null type as an input type, your resolver will never recieve null and consumers of your GraphQL API must provide a value for it unless you provide a default value.
g.field({args: {someNonNullAndRequiredArg: g.arg({type: g.nonNull(g.String),}),someNonNullButOptionalArg: g.arg({type: g.nonNull(g.String),defaultValue: "some default",}),},type: g.String,resolve(source, args) {// both of these will always be a stringargs.someNonNullAndRequiredArg;args.someNonNullButOptionalArg;return "";},});// ==graphql`fieldName(someNonNullAndRequiredArg: String!someNonNullButOptionalArg: String! = "some default"): String`;
When using a non-null type as an output type, your resolver must never
return null. If you do return null(which unless you do
type-casting/ts-ignore/etc. @graphql-ts/schema
will not let you do)
graphql-js will return an error to consumers of your GraphQL API.
Non-null types should be used very carefully on output types. If you have to do a fallible operation like a network request or etc. to get the value, it probably shouldn't be non-null. If you make a field non-null and doing the fallible operation fails, consumers of your GraphQL API will be unable to see any of the other fields on the object that the non-null field was on. For example, an id on some type is a good candidate for being non-null because if you have the item, you will already have the id so getting the id will never fail but fetching a related item from a database would be fallible so even if it will never be null in the success case, you should make it nullable.
g.field({type: g.nonNull(g.String),resolve(source, args) {return "something";},});// ==graphql`fieldName: String!`;
If you try to wrap another non-null type in a non-null type again, you will get a type error.
// Argument of type 'NonNullType<ScalarType<string>>'// is not assignable to parameter of type 'NullableType'.g.nonNull(g.nonNull(g.String));
Creates a scalar type.
const BigInt = g.scalar({name: "BigInt",serialize(value) {if (typeof value !== "bigint")throw new GraphQLError(`unexpected value provided to BigInt scalar: ${value}`);return value.toString();},parseLiteral(value) {if (value.kind !== "StringValue")throw new GraphQLError("BigInt only accepts values as strings");return globalThis.BigInt(value.value);},parseValue(value) {if (typeof value === "bigint") return value;if (typeof value !== "string")throw new GraphQLError("BigInt only accepts values as strings");return globalThis.BigInt(value);},});// for fields on output typesg.field({ type: someScalar });// for args on output fields or fields on input typesg.arg({ type: someScalar });
Note, while graphql-js allows you to express scalar types like the ID
type which accepts integers and strings as both input values and return
values from resolvers which are transformed into strings before calling
resolvers and returning the query respectively, the type you use should be
string
for ID
since that is what it is transformed into.
@graphql-ts/schema
doesn't currently express the coercion of scalars, you
should instead convert values to the canonical form yourself before
returning from resolvers.
A GraphQL object type. This should generally be constructed with object.
Note this is an output type, if you want an input object, use GInputObjectType.
If you use the GObjectType
constructor directly, all fields will need
explicit resolvers so you should use g.object
instead.
A GraphQL enum type. This should generally be constructed with enum.
Unlike some other constructors in this module, this constructor functions
exactly the same as it's counterpart g.enum
so it is safe to use directly
if desired.
A GraphQL enum type. This should generally be constructed with scalar.
Unlike some other constructors in this module, this constructor functions
exactly the same as it's counterpart g.scalar
so it is safe to use directly
if desired.
Also unlike some other types in this module, this type is exactly equivalent
to the original GraphQLScalarType
type from the
graphql
package.
A GraphQL input object type. This should generally be constructed with inputObject.
Unlike some other constructors in this module, this constructor functions
exactly the same as it's counterpart g.inputObject
so it is safe to use
directly if desired.
A GraphQL interface type that can be implemented by other GraphQL object and interface types. This should generally be constructed with interface.
If you use the GInterfaceType
constructor directly, all fields will need
explicit resolvers so you should use g.interface
instead.
A GraphQL union type. This should generally be constructed with union.
A union type represents an object that could be one of a list of types. Note it is similar to an GInterfaceType except that a union doesn't imply having a common set of fields among the member types.
While this constructor will work, you should generally use g.union
because
you will need to explicitly provide the source type parameter as TypeScript
is unable to infer it correctly. Note this is only required for this
constructor, this is not required when using g.union
.
A GraphQL list type. This should generally be constructed with list.
Unlike some other constructors in this module, this constructor functions
exactly the same as it's counterpart g.list
so it is safe to use directly
if desired.
Also unlike the named types in this module, the original
GraphQLList
type from the graphql
package cannot be
assigned to a variable of type GList
. Though GList
is assignable to
GraphQLList
.
For example, the following code will not compile:
const list: GList<GScalarType<string>> = new GraphQLList(GraphQLString);
But the following code will compile:
const list: GraphQLList<GraphQLScalarType<string>> = new GList(GraphQLString);
This is due to the lack of a discriminating property between the
GraphQLNonNull
and GraphQLList
types.
A GraphQL non-null type. This should generally be constructed with nonNull.
Unlike some other constructors in this module, this constructor functions
exactly the same as it's counterpart g.nonNull
so it is safe to use
directly if desired.
Also unlike the named types in this module, the original
GraphQLNonNull
type from the graphql
package cannot
be assigned to a variable of type GNonNull
. Though GNonNull
is
assignable to GraphQLNonNull
.
For example, the following code will not compile:
const nonNull: GNonNull<GScalarType<string>> = new GraphQLNonNull(GraphQLString);
But the following code will compile:
const nonNull: GraphQLNonNull<GraphQLScalarType<string>> = new GNonNull(GraphQLString);
This is due to the lack of a discriminating property between the
GraphQLNonNull
and GraphQLList
types.
A GraphQL argument. These should be created with arg
Args can can be used as arguments on output fields:
g.field({type: g.String,args: {something: g.arg({ type: g.String }),},resolve(source, { something }) {return something || somethingElse;},});// ==graphql`fieldName(something: String): String`;
Or as fields on input objects:
g.inputObject({name: "Something",fields: {something: g.arg({ type: g.String }),},});// ==graphql`input Something {something: String}`;
When the type of an arg is non-null, the value will always exist.
g.field({type: g.String,args: {something: g.arg({ type: g.nonNull(g.String) }),},resolve(source, { something }) {// `something` will always be a stringreturn something;},});// ==graphql`fieldName(something: String!): String`;
A GraphQL output field for an object type which should be created using field.
export type GField<Source, Args extends { }, Type extends GOutputType<Context>, SourceAtKey, Context> = {