Skip to content

Typed Documents ​

Although GraphQL defines conventions and guarantees for the client-side GraphQL query language and the server-side GraphQL type system, it’s still ultimately an API specifaction for client-server applications.

In GraphQL, we create schemas that describe the type system that queries are executed against, and said schema describes a shape of GraphQL types and scalars. As such, even if we use strong types on the client-side and strong types on the server-side, we still have to bridge the gap between both ends.

Schemas and Queries ​

Given a GraphQL schema, expressed here in the Schema Definition Language (“SDL”), in its simplest form, we define fields on objects that, when queried, may resolve to scalar values.

graphql
type Query {
  helloWorld: String
  numberOfRequests: Int!
}

When querying this schema we may write a query that requests our defined fields:

graphql
query {
  helloWorld
  numberOfRequests
}

Simplifying this, a GraphQL query, which has been validated against a GraphQL schema, matches a subset of the structure our GraphQL schema defines. However, while it “selects” fields and defines how to execute their resolvers, type information is only present on the schema.

As such, as per the specification, a GraphQL API with the above SDL may only return data matching our query, such as:

json
{
  "helloWorld": "Hello!",
  "numberOfRequests: 1
}

It’s the server-side type system’s responsibility to ensure that when this schema is executed against a valid query, that the execution result matches the types defined.

As such, while the server-side, written in any library for any language implementing the GraphQL specification, has complete knowledge of the schemas types and structure, queries are subject to these types implicitly.

Type Generation ​

If we’re using TypeScript on the client-side and have a set of GraphQL documents that we may execute against our schema, we can only know the documents’ result types by looking comparing them to the schema.

In GraphQL, this is often done ahead of time in with “Type Generation”. This means that we input our schema and our queries into a process at compile-time and convert the shape of the query to TypeScript’s type system.

As such, the shape of data in the queries above would match a TypeScript type looking like the following:

ts
interface Result {
  helloWorld: string | null;
  numberOfRequests: number;
}

In many tools this is done using code generation, a process that, like many concepts in GraphQL, has already been established with JSON Schemas, or other API-shape specifications. In code generation, we would have to ensure that a compile-time tool generates or connects our TypeScript type to what we use at runtime — a query string or AST.

With gql.tada, this all happens in the TypeScript type system and is more invisible since no files are automatically generated per GraphQL document.

Integration with Clients ​

If we don’t integrate GraphQL on the client-side with Type Generation, then a minimal example using GraphQL is quite simple.

In this example, we’ll have a GraphQL query sent to an API using a simple fetch call:

ts
import { DocumentNode, 
parse
,
print
} from 'graphql';
const
query
=
parse
(/* GraphQL */ `
{ helloWorld numberOfRequests } `); async function
execute
(
query
: DocumentNode,
variables
?: any): Promise<any> {
const response = await fetch('/graphql', { method: 'POST',
body
: JSON.stringify({
query
:
print
(
query
),
variables
,
}), }); return (await
response
.json()).data;
} const
data
= await
execute
(
query
);

In TypeScript however, we’re lacking two vital integration points here. Without Type Generations, neither data nor variables are typed according to our GraphQL schema, although with GraphQL’s guarantees these types should be unambiguous.

Manual Type Generation ​

With manual code generation tools, a separate tool would output a file containing our query’s types. For instance, it may output a separate file that contains the Result and Variables types we need:

ts
export type 
Result
= {
helloWorld
: string | null;
numberOfRequests
: number;
}; export type
Variables
= {};

However, this now requires us to make an effort to include these types manually in our execute function:

ts
import { DocumentNode, 
parse
,
print
} from 'graphql';
import type {
Result
,
Variables
} from './query.generated';
const
query
=
parse
(/* GraphQL */ `
{ helloWorld numberOfRequests } `);
We add generics to our function:
async function
execute
<
Result
= any,
Variables
= any>(
query
: DocumentNode,
variables
:
Variables
): Promise<
Result
> {
const
response
= await fetch('/graphql', {
method: 'POST',
body
: JSON.stringify({
query
:
print
(
query
),
variables
,
}), }); return (await
response
.json()).data;
} const
data
= await
execute
<
Result
,
Variables
>(
query
, {});
We pass our generated types to these generics

This is a very manual process though that doesn’t match our expectation that GraphQL is strongly typed and types should hence be inferred implicitly.

TypedDocumentNode types ​

What we really wish to do with client-side GraphQL is to attach our generated types to the DocumentNode type directly. This would mean that our query above can only lead to the correct types being used. Furthermore, by using said query, its types could be inferred automatically.

Instead of having a DocumentNode type, ideally, we’d want the query to be typed as TypedDocumentNode<Result, Variables>.

As a result, GraphQL in TypeScript has two ways of attaching types to GraphQL documents:

Our type generation tools can now output a TypedDocumentNode that has types attached to it directly:

ts
import { 
parse
} from 'graphql';
import {
TypedDocumentNode
} from '@graphql-typed-document-node/core';
type
Result
= {
helloWorld
: string | null;
numberOfRequests
: number;
}; type
Variables
= {};
export const
query
:
TypedDocumentNode
<
Result
,
Variables
> =
parse
(
'{ helloWorld, numberOfRequests }' );

Which for clients executing a GraphQL query means, that they can infer the types of a given GraphQL query by matching it against TypedDocumentNode instead. In our example, we can now infer the generic types from the query instead:

ts
import { 
TypedDocumentNode
} from '@graphql-typed-document-node/core';
import { DocumentNode,
parse
,
print
} from 'graphql';
import {
query
} from './query.generated';
async function
execute
<
Result
= any,
Variables
= any>(
query
:
TypedDocumentNode
<
Result
,
Variables
>,
variables
:
Variables
): Promise<
Result
> {
const
response
= await fetch('/graphql', {
method: 'POST',
body
: JSON.stringify({
query
:
print
(
query
),
variables
,
}), }); return (await
response
.json()).data;
} const
data
= await
execute
(
query
, {});
Types are now inferred from the query argument

gql.tada type inference ​

Having types output by a separate code generation tool again doesn’t quite match our expectation that GraphQL is strongly typed and types should hence be inferred implicitly.

In gql.tada however, the idea is that we get from writing a query to having a TypedDocumentNode type just via TypeScript inference, without running a separate tool or having files be generated for each query.

ts
import { 
graphql
} from 'gql.tada';
We get a TypedDocumentNode without extra imports
const
query
=
graphql
(`
{ helloWorld numberOfRequests } `);

In essence, what gql.tada does is give you a fully typed GraphQL document that tells TypeScript what the Result and Variables types are just via inference.

Client Support ​

Today, supporting typed documents in GraphQL is an accepted and de-facto standard, and below you can find a non-exhaustive list of GraphQL clients that support typed documents and will hence also work well with gql.tada.

ts
import { 
graphql
} from 'gql.tada';
import {
useQuery
} from '@apollo/client';
const
getPokemonsQuery
=
graphql
(`
query GetPokemons { pokemons { id name } } `); function
Pokemons
() {
const {
loading
,
error
,
data
} =
useQuery
(
getPokemonsQuery
);
}
ts
import { 
graphql
} from 'gql.tada';
const
getPokemonsQuery
=
graphql
(`
query GetPokemons { pokemons { id name } } `); async function
getPokemons
() {
const {
data
} = await
client
.
query
(
getPokemonsQuery
, {});
}
ts
import { 
graphql
} from 'gql.tada';
import {
useQuery
} from 'urql';
const
getPokemonsQuery
=
graphql
(`
query GetPokemons { pokemons { id name } } `); function
Pokemons
() {
const [{
fetching
,
error
,
data
}] =
useQuery
({
query
:
getPokemonsQuery
});
}
ts
import { 
graphql
} from 'gql.tada';
import
request
from 'graphql-request';
const
getPokemonsQuery
=
graphql
(`
query GetPokemons { pokemons { id name } } `); async function
getPokemons
() {
const
data
= await
request
('/graphql',
getPokemonsQuery
);
}