@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 { graphql } from "@graphql-ts/schema";import { GraphQLSchema, graphql as runGraphQL } from "graphql";const Query = graphql.object()({name: "Query",fields: {hello: graphql.field({type: graphql.String,resolve() {return "Hello!";},}),},});const schema = new GraphQLSchema({query: Query.graphQLType,});runGraphQL({source: `query {hello}`,schema,}).then((result) => {console.log(result);});
The graphql
export is the primary entrypoint into @graphql-ts/schema
that
lets you compose GraphQL types into a GraphQL Schema
A simple schema with only a query type looks like this.
import { graphql } from "@graphql-ts/schema";import { GraphQLSchema, graphql as runGraphQL } from "graphql";const Query = graphql.object()({name: "Query",fields: {hello: graphql.field({type: graphql.String,resolve() {return "Hello!";},}),},});const schema = new GraphQLSchema({query: Query.graphQLType,});runGraphQL({source: `query {hello}`,schema,}).then((result) => {console.log(result);});
You can use pass the graphQLSchema
to ApolloServer
and other GraphQL servers.
You can also create a more advanced schema with other object types, args and mutations.
import { graphql } from "@graphql-ts/schema";import { GraphQLSchema, graphql as runGraphQL } from "graphql";import { deepEqual } from "assert";type TodoItem = {title: string;};const todos: TodoItem[] = [];const Todo = graphql.object<TodoItem>({name: "Todo",fields: {title: graphql.field({ type: graphql.String }),},});const Query = graphql.object()({name: "Query",fields: {todos: graphql.field({type: graphql.list(Todo),resolve() {return todos;},}),},});const Mutation = graphql.object()({name: "Mutation",fields: {createTodo: graphql.field({args: {title: graphql.arg({ type: graphql.String }),},type: Todo,resolve(source, { title }) {const todo = { title };todos.push(todo);return todo;},}),},});const schema = new GraphQLSchema({query: Query.graphQLType,mutation: Mutation.graphQLType,});(async () => {const result = await runGraphQL({source: `query {todos {title}}`,schema,});deepEqual(result, { data: { todos: [] } });const result = await runGraphQL({source: `mutation {createTodo(title: "Try@graphql-ts — /schema") {title}}`,schema,});deepEqual(result, {data: { createTodo: { title: "Try@graphql-ts — /schema" } },});const result = await runGraphQL({source: `query {todos {title}}`,schema,});deepEqual(result, {data: { todos: [{ title: "Try@graphql-ts — /schema" }] },});})();
For information on how to construct other types like input objects, unions,
interfaces and enums and more detailed information, see the documentation in
the graphql
export below.
When using it, you're going to want to create your own version of it bound to
your specific Context
type.
Creates a GraphQL field.
These will generally be passed directly to the fields
object in a
graphql.object
call.
const Something = graphql.object<{ thing: string }>()({name: "Something",fields: {thing: graphql.field({ type: graphql.String }),},});
Creates a GraphQL object type.
Note this is an output type, if you want an input object, use
graphql.inputObject
.
When calling graphql.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 = graphql.object<{ name: string }>()({name: "Person",fields: {name: graphql.field({ type: graphql.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 = graphql.object<{ name: string }>()({name: "Person",fields: {name: graphql.field({ type: graphql.String }),excitedName: graphql.field({type: graphql.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
graphql.ObjectType<Source>
along with making fields
a function that
returns the object.
type PersonSource = { name: string; friends: PersonSource[] };const Person: graphql.ObjectType<PersonSource> =graphql.object<PersonSource>()({name: "Person",fields: () => ({name: graphql.field({ type: graphql.String }),friends: graphql.field({ type: graphql.list(Person) }),}),});
Creates a GraphQL argument.
Args can can be used as arguments on output fields:
graphql.field({type: graphql.String,args: {something: graphql.arg({ type: graphql.String }),},resolve(source, { something }) {return something || somethingElse;},});// ==graphql`(something: String): String`;
Or as fields on input objects:
const Something = graphql.inputObject({name: "Something",fields: {something: graphql.arg({ type: graphql.String }),},});// ==graphql`input Something {something: String}`;
Creates an InputObjectType
const Something = graphql.inputObject({name: "Something",fields: {something: graphql.arg({ type: graphql.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.
type SomethingInputType = graphql.InputObjectType<{something: graphql.Arg<SomethingInputType>;}>;const Something: SomethingInputType = graphql.inputObject({name: "Something",fields: () => ({something: graphql.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.
const nonCircularFields = {thing: graphql.arg({ type: graphql.String }),};type SomethingInputType = graphql.InputObjectType<typeof nonCircularFields & {something: graphql.Arg<SomethingInputType>;}>;const Something: SomethingInputType = graphql.inputObject({name: "Something",fields: () => ({...nonCircularFields,something: graphql.arg({ type: Something }),}),});
Wraps any GraphQL type in a ListType.
const stringListType = graphql.list(graphql.String);// ==graphql`[String]`;
When used as an input type, you will recieve an array of the inner type.
graphql.field({type: graphql.String,args: { thing: graphql.arg({ type: graphql.list(graphql.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'
.
graphql.field({type: graphql.list(graphql.String),resolve() {return [""];},});
graphql.field({type: graphql.list(graphql.String),resolve() {return new Set([""]);},});
graphql.field({type: graphql.list(graphql.String),resolve() {// this will not be allowedreturn "some things";},});
Wraps a NullableType with a NonNullType.
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 = graphql.nonNull(graphql.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.
graphql.field({args: {someNonNullAndRequiredArg: graphql.arg({type: graphql.nonNull(graphql.String),}),someNonNullButOptionalArg: graphql.arg({type: graphql.nonNull(graphql.String),defaultValue: "some default",}),},type: graphql.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.
graphql.field({type: graphql.nonNull(graphql.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 'TypesExcludingNonNull'.graphql.nonNull(graphql.nonNull(graphql.String));
Creates an enum type with a number of enum values.
const MyEnum = graphql.enum({name: "MyEnum",values: graphql.enumValues(["a", "b"]),});// ==graphql`enum MyEnum {ab}`;
const MyEnum = graphql.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")}`;)
A shorthand to easily create enum values to pass to
graphql.enum
.
If you need to set a description
or deprecationReason
for an enum
variant, you should pass values directly to graphql.enum
without using
graphql.enumValues
.
const MyEnum = graphql.enum({name: "MyEnum",values: graphql.enumValues(["a", "b"]),});
const values = graphql.enumValues(["a", "b"]);assertDeepEqual(values, {a: { value: "a" },b: { value: "b" },});
A helper to easily share fields across object and interface types.
const nodeFields = graphql.fields<{ id: string }>()({id: graphql.field({ type: graphql.ID }),});const Node = graphql.field({name: "Node",fields: nodeFields,});const Person = graphql.object<{__typename: "Person";id: string;name: string;}>()({name: "Person",interfaces: [Node],fields: {...nodeFields,name: graphql.field({ type: graphql.String }),},});
graphql.fields
instead of just creating an object?The definition of Field in @graphql-ts/schema
has some special things,
let's look at the definition of it:
type Field<Source,Args extends Record<string, Arg<InputType>>,TType extends OutputType<Context>,Key extends string,Context> = ...;
There's two especially notable bits in there which need to be inferred from
elsewhere, the Source
and Key
type params.
The Source
is pretty simple and it's quite simple to see why
graphql.fields
is useful here. You could explicitly write it with
resolvers on the first arg but you'd have to do that on every field which
would get very repetitive and wouldn't work for fields without resolvers.
const someFields = graphql.fields<{ name: string }>()({name: graphql.field({ type: graphql.String }),});
The Key
type param might seem a bit more strange though. What it's saying
is that the key that a field is at is part of its TypeScript type.
This is important to be able to represent the fact that a resolver is
optional if the Source
has a property at the Key
that matches the output type.
// this is allowedconst someFields = graphql.fields<{ name: string }>()({name: graphql.field({ type: graphql.String }),});const someFields = graphql.fields<{ name: string }>()({someName: graphql.field({// a resolver is required here since the Source is missing a `someName` propertytype: graphql.String,}),});
Note that there is no similar function for args since they don't need special type parameters like Field does so you can create a regular object and put args in it if you want to share them.
Creates a GraphQL interface type that can be implemented by other GraphQL object and interface types.
const Entity = graphql.interface()({name: "Entity",fields: {name: graphql.interfaceField({ type: graphql.String }),},});type PersonSource = { __typename: "Person"; name: string };const Person = graphql.object<PersonSource>()({name: "Person",interfaces: [Entity],fields: {name: graphql.field({ type: graphql.String }),},});type OrganisationSource = {__typename: "Organisation";name: string;};const Organisation = graphql.object<OrganisationSource>()({name: "Organisation",interfaces: [Entity],fields: {name: graphql.field({ type: graphql.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 graphql.interfaceField
was used instead of
graphql.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 graphql.fields
to do that. See graphql.fields
for more
information about why you should use graphql.fields
instead of just
defining an object the fields and spreading that.
const nodeFields = graphql.fields<{ id: string }>({id: graphql.field({ type: graphql.ID }),});const Node = graphql.field({name: "Node",fields: nodeFields,});const Person = graphql.object<{__typename: "Person";id: string;name: string;}>()({name: "Person",interfaces: [Node],fields: {...nodeFields,name: graphql.field({ type: graphql.String }),},});
Creates a GraphQL interface field.
These will generally be passed directly to fields
object in a
graphql.interface
call. Interfaces fields are
similar to regular fields except that they don't define how
the field is resolved.
const Entity = graphql.interface()({name: "Entity",fields: {name: graphql.interfaceField({ type: graphql.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.
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 InterfaceType except that a union doesn't imply having a common set of fields among the member types.
const A = graphql.object<{ __typename: "A" }>()({name: "A",fields: {something: graphql.field({ type: graphql.String }),},});const B = graphql.object<{ __typename: "B" }>()({name: "B",fields: {differentThing: graphql.field({ type: graphql.String }),},});const AOrB = graphql.union({name: "AOrB",types: [A, B],});
Any GraphQL input type.
Note that this is not generic over a Context
like OutputType is
because inputs types never interact with the Context
.
See also:
Wraps any GraphQL type in a list type.
See the documentation of graphql.list
for more information.
Wraps a NullableType with a non-null type.
See the documentation for graphql.nonNull
for more information.
Any nullable GraphQL input type.
Note that this is not generic over a Context
like
NullableOutputType
is because inputs types never
interact with the Context
.
See also:
Any input list type. This type only exists because of limitations in circular types.
If you want to represent any list input type, you should do ListType<InputType>
.
A GraphQL scalar type which wraps an underlying graphql-js
GraphQLScalarType
with a type representing the deserialized form of the
scalar. These should be created used graphql.scalar
.
const someScalar = graphql.scalar<string>(new GraphQLScalarType({}));// for fields on output typesgraphql.field({ type: someScalar });// for args on output fields or fields on input typesgraphql.arg({ type: someScalar });
A GraphQL enum type which wraps an underlying graphql-js
GraphQLEnumType. This should be created with graphql.enum
.
const MyEnum = graphql.enum({name: "MyEnum",values: graphql.enumValues(["a", "b"]),});// ==graphql`enum MyEnum {ab}`;
An individual enum value in an enum type created using
graphql.enum
. You can use the
graphql.enumValues
shorthand to create enum values more easily.
Note the value property/generic here represents the deserialized form of the enum. It does not indicate the name of the enum value that is visible in the GraphQL schema. The value can be anything, not necessarily a string. Usually though, it will be a string which is equal to the key where the value is used.
A GraphQL argument. These should be created with graphql.arg
Args can can be used as arguments on output fields:
graphql.field({type: graphql.String,args: {something: graphql.arg({ type: graphql.String }),},resolve(source, { something }) {return something || somethingElse;},});// ==graphql`fieldName(something: String): String`;
Or as fields on input objects:
graphql.inputObject({name: "Something",fields: {something: graphql.arg({ type: graphql.String }),},});// ==graphql`input Something {something: String}`;
When the type of an arg is non-null, the value will always exist.
graphql.field({type: graphql.String,args: {something: graphql.arg({ type: graphql.nonNull(graphql.String) }),},resolve(source, { something }) {// `something` will always be a stringreturn something;},});// ==graphql`fieldName(something: String!): String`;
Creates a ScalarType from a graphql-js GraphQLScalarType.
You should provide a type as a type parameter which is the type of the scalar
value. 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.
const JSON = graphql.scalar<JSONValue>(GraphQLJSON);// for fields on output typesgraphql.field({ type: someScalar });// for args on output fields or fields on input typesgraphql.arg({ type: someScalar });
The particular Context
type for this graphql
export that is provided to
field resolvers.
Below, there are many types exported which are similar to the types with the
same names exported from the root of the package except that they are bound
to the Context
type so they can be used elsewhere without needing to be
bound to the context on every usage.
A GraphQL output field for an ObjectType which should be created using
graphql.field
.
Any nullable GraphQL output type for a given Context
.
See also:
Any output list type. This type only exists because of limitations in circular types.
If you want to represent any list input type, you should do
ListType<OutputType<Context>>
.
A GraphQL object type which should be created using graphql.object
.
Creates a GraphQL object type.
See the docs of graphql.object
for more details.
A description of the object type that is visible when introspected.
type Person = { name: string };const Person = graphql.object<Person>()({name: "Person",description: "A person does things!",fields: {name: graphql.field({ type: graphql.String }),},});// ==graphql`"""A person does things!"""type Person {name: String}`;
A number of interfaces that the object type implements. See
graphql.interface
for more information.
const Node = graphql.interface<{ kind: string }>()({name: "Node",resolveType: (source) => source.kind,fields: {id: graphql.interfaceField({ type: graphql.ID }),},});const Person = graphql.object<{ kind: "Person"; id: string }>()({name: "Person",interfaces: [Node],fields: {id: graphql.field({ type: graphql.ID }),},});
Creates a GraphQL object type.
Note this is an output type, if you want an input object, use
graphql.inputObject
.
When calling graphql.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 = graphql.object<{ name: string }>()({name: "Person",fields: {name: graphql.field({ type: graphql.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 = graphql.object<{ name: string }>()({name: "Person",fields: {name: graphql.field({ type: graphql.String }),excitedName: graphql.field({type: graphql.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
graphql.ObjectType<Source>
along with making fields
a function that
returns the object.
type PersonSource = { name: string; friends: PersonSource[] };const Person: graphql.ObjectType<PersonSource> =graphql.object<PersonSource>()({name: "Person",fields: () => ({name: graphql.field({ type: graphql.String }),friends: graphql.field({ type: graphql.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 InterfaceType except that a union doesn't imply having a common set of fields among the member types.
const A = graphql.object<{ __typename: "A" }>()({name: "A",fields: {something: graphql.field({ type: graphql.String }),},});const B = graphql.object<{ __typename: "B" }>()({name: "B",fields: {differentThing: graphql.field({ type: graphql.String }),},});const AOrB = graphql.union({name: "AOrB",types: [A, B],});
Creates a GraphQL field.
These will generally be passed directly to the fields
object in a
graphql.object
call.
const Something = graphql.object<{ thing: string }>()({name: "Something",fields: {thing: graphql.field({ type: graphql.String }),},});
A helper to easily share fields across object and interface types.
const nodeFields = graphql.fields<{ id: string }>()({id: graphql.field({ type: graphql.ID }),});const Node = graphql.field({name: "Node",fields: nodeFields,});const Person = graphql.object<{__typename: "Person";id: string;name: string;}>()({name: "Person",interfaces: [Node],fields: {...nodeFields,name: graphql.field({ type: graphql.String }),},});
graphql.fields
instead of just creating an object?The definition of Field in @graphql-ts/schema
has some special things,
let's look at the definition of it:
type Field<Source,Args extends Record<string, Arg<InputType>>,TType extends OutputType<Context>,Key extends string,Context> = ...;
There's two especially notable bits in there which need to be inferred from
elsewhere, the Source
and Key
type params.
The Source
is pretty simple and it's quite simple to see why
graphql.fields
is useful here. You could explicitly write it with
resolvers on the first arg but you'd have to do that on every field which
would get very repetitive and wouldn't work for fields without resolvers.
const someFields = graphql.fields<{ name: string }>()({name: graphql.field({ type: graphql.String }),});
The Key
type param might seem a bit more strange though. What it's saying
is that the key that a field is at is part of its TypeScript type.
This is important to be able to represent the fact that a resolver is
optional if the Source
has a property at the Key
that matches the output type.
// this is allowedconst someFields = graphql.fields<{ name: string }>()({name: graphql.field({ type: graphql.String }),});const someFields = graphql.fields<{ name: string }>()({someName: graphql.field({// a resolver is required here since the Source is missing a `someName` propertytype: graphql.String,}),});
Note that there is no similar function for args since they don't need special type parameters like Field does so you can create a regular object and put args in it if you want to share them.
Creates a GraphQL interface field.
These will generally be passed directly to fields
object in a
graphql.interface
call. Interfaces fields are
similar to regular fields except that they don't define how
the field is resolved.
const Entity = graphql.interface()({name: "Entity",fields: {name: graphql.interfaceField({ type: graphql.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 = graphql.interface()({name: "Entity",fields: {name: graphql.interfaceField({ type: graphql.String }),},});type PersonSource = { __typename: "Person"; name: string };const Person = graphql.object<PersonSource>()({name: "Person",interfaces: [Entity],fields: {name: graphql.field({ type: graphql.String }),},});type OrganisationSource = {__typename: "Organisation";name: string;};const Organisation = graphql.object<OrganisationSource>()({name: "Organisation",interfaces: [Entity],fields: {name: graphql.field({ type: graphql.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 graphql.interfaceField
was used instead of
graphql.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 graphql.fields
to do that. See graphql.fields
for more
information about why you should use graphql.fields
instead of just
defining an object the fields and spreading that.
const nodeFields = graphql.fields<{ id: string }>({id: graphql.field({ type: graphql.ID }),});const Node = graphql.field({name: "Node",fields: nodeFields,});const Person = graphql.object<{__typename: "Person";id: string;name: string;}>()({name: "Person",interfaces: [Node],fields: {...nodeFields,name: graphql.field({ type: graphql.String }),},});
Any GraphQL type for a given Context
.
Note that this includes both input and output types.
You generally won't need this because you'll likely want an
input or output type but there are some
uses cases for it like graphql.list
.
See also:
Any nullable GraphQL type for a given Context
.
You generally won't need this because you'll likely want a nullable
input or output type but
there are some uses cases for it like graphql.nonNull
.
See also: