Writing GraphQL
In gql.tada
, we write our GraphQL documents using the graphql()
function.
Queries
When passing a query to graphql()
, it will be parsed in TypeScript’s type system
and the schema that’s set up is used to map this document over to a type.
The TodosQuery
variable will have an inferred type that defines the
type of the data result of the query. When adding variables, the types of variables
are added to the inferred type as well.
The resulting type is known as a TypedDocumentNode
and is supported by most GraphQL clients.
When passing a gql.tada
query to a GraphQL client, the type
of input variables and result data are inferred automatically.
For example, with urql
and React, this may look like the following:
The same applies to mutation operations, subscription operations, and fragment definitions.
The graphql()
function will parse your GraphQL definitions, take the first definition it
finds and infers its type automatically.
The above example uses the ResultOf
and VariablesOf
types for illustrative purposes.
These type utilities may be used to manually unwrap the types of a GraphQL DocumentNode
returned by graphql()
.
Fragments
The graphql()
function allows for fragment composition, which means we’re able to create
a fragment and spread it into our definitions or other fragments.
Creating a fragment is the same as any other operation definition. The type of the first definition, in this case a fragment, will be used to infer the result type of the returned document:
Spreading this fragment into another fragment or operation definition requires us
to pass the fragment into a tuple array on the graphql()
function’s second argument.
Here we spread our TodoItemFragment
into into TodosQuery
by passing it into
the graphql()
function and then using its name in the GraphQL document.
Fragment Masking
However, in gql.tada
a pattern called “Fragment Masking” applies.
TodosQuery
’s result type does not contain the title
and comleted
field
from the spread fragment and instead contains a reference to the TodoItemFragment
.
This forces us to unwrap, or rather “unmask”, the fragment first.
When spreading a fragment into a parent definition, the parent only contains a reference to the fragment. This means that we’re isolating fragments. Any spread fragment data cannot be accessed directly until the fragment is unmasked.
TodoItem
’s fragment mask in TodosQuery
is only unmasked and accessible as its plain result
type once we call readFragment()
on the fragment mask.
In this case, we’re passing result.todos
, which is a list of the objects containing the fragment
mask.
This all only happens and is enforced at a type level, meaning that we don’t incur any overhead during runtime for masking our fragments.
Fragment Composition
Fragment Masking is a concept that only exists to enforce proper Fragment Composition.
In a componentized app, fragments may be used to define the data requirements of UI components, which means, we’ll define fragments, colocate them with our components, and compose them into other fragments or our query.
Since all fragments are masked in our types, this colocation is enforced and we maintain our data requirements to UI component relationship.
For example, our TodoItemFragment
may be associated with a TodoItem
component rendering
individual items:
The FragmentOf
type is used as an input type above. This type accepts our fragment document
and creates the fragment mask that a fragment spread would create as well.
We can then use our new TodoItemComponent
in our TodosListComponent
and compose its TodoItemFragment
into our query:
Meaning, while we can unmask and use the TodoItemFragment
’s data in the TodoItemComponent
,
the TodoListComponent
cannot access any of the data requirements defined by and meant for the
TodoItemComponent
.