Skip to content

Commit

Permalink
Add content for Typed Documents page
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten committed Jan 24, 2024
1 parent 46dbd64 commit 47b1659
Showing 1 changed file with 369 additions and 0 deletions.
369 changes: 369 additions & 0 deletions website/src/content/docs/guides/typed-documents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,372 @@
title: Typed Documents
description: How GraphQL documents and TypeScript come together
---

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 title="query.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 title="query.generated.ts"
export interface Result {
helloWorld: string | null;
numberOfRequests: number;
}

export interface Variables {}
```

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

```ts title="query.ts" {"We add generics to our function:":8-12} {"We set the generics to our generated types:":23-24}
import { DocumentNode, parse, print } from 'graphql';
import type { Result, Variables } from './query.generated.ts';

const query = parse(/* GraphQL */ `
{ helloWorld, numberOfRequests }
`);


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);
```

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:
- the [`@graphql-typed-document-node/core`](https://github.com/dotansimha/graphql-typed-document-node) type
- the [`graphql` package’s](https://github.com/graphql/graphql-js/blob/2aedf25/src/utilities/typedQueryDocumentNode.ts) type

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

```ts title="query.generated.ts"
import { TypedDocumentNode } from '@graphql-typed-document-node/core';

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

interface 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 title="query.ts" {"Types are now inferred from the query argument:":4-11}
import { DocumentNode, parse, print } from 'graphql';
import { query } from './query.generated.ts';


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);
```

### `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 {"This is a TypedDocumentNode without extra imports:":3-6}
import { graphql } from 'gql.tada';


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.

## Clients supporting types

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`.

### `@apollo/client`

```tsx
import { graphql } from 'gql.tada';
import { useQuery } from '@apollo/client';

const getBooksQuery = graphql(`
query GetBooks {
books {
id
title
}
}
`);

function Books() {
const { loading, error, data } = useQuery(getBooksQuery);
return null; // ...
}
```

### `@urql/core`

```ts
import { graphql } from 'gql.tada';
import { useQuery } from 'urql';

const getBooksQuery = graphql(`
query GetBooks {
books {
id
title
}
}
`);

async function getBooks() {
const { data } = await client.query(getBooks);
// ...
}
```

### `urql`

```tsx
import { graphql } from 'gql.tada';
import { useQuery } from 'urql';

const getBooksQuery = graphql(`
query GetBooks {
books {
id
title
}
}
`);

function Books() {
const { loading, error, data } = useQuery({ query: getBooksQuery });
return null; // ...
}
```

### `graphql-request`

```ts
import { graphql } from 'gql.tada';
import request from 'graphql-request';

const getBooksQuery = graphql(`
query GetBooks {
books {
id
title
}
}
`);

async function getBooks() {
const data = await request('/graphql', getBooksQuery);
// ...
}
```

### `villus`

```vue
<script setup>
import { graphql } from 'gql.tada';
import { useQuery } from 'villus';
const getBooksQuery = graphql(`
query GetBooks {
books {
id
title
}
}
`);
const { data } = useQuery({
query: AllPosts,
});
</script>
```

0 comments on commit 47b1659

Please sign in to comment.