Build Typed GraphQL Queries in TypeScript. A better TypeScript + GraphQL experience.
npm install --save typed-graphqlify
Or if you use Yarn:
yarn add typed-graphqlify
We all know that GraphQL is so great and solves many problems that we have with REST APIs, like overfetching and underfetching. But developing a GraphQL Client in TypeScript is sometimes a bit of pain. Why? Let's take a look at the example we usually have to make.
When we use GraphQL library such as Apollo, We have to define a query and its interface like this:
interface GetUserQueryData {
getUser: {
id: number
name: string
bankAccount: {
id: number
branch?: string
}
}
}
const query = graphql(gql`
query getUser {
user {
id
name
bankAccount {
id
branch
}
}
}
`)
apolloClient.query<GetUserQueryData>(query).then(data => ...)
This is so painful.
The biggest problem is the redundancy in our codebase, which makes it difficult to keep things in sync. To add a new field to our entity, we have to care about both GraphQL and TypeScript interface. And type checking does not work if we do something wrong.
typed-graphqlify comes in to address this issues, based on experience from over a dozen months of developing with GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using GraphQL-like object and a bit of helper class. Additional features including graphql-tag, or Fragment can be implemented by other tools like Apollo.
First, define GraphQL-like JS Object:
import { params, types } from 'typed-graphqlify'
const getUserQuery = {
user: params(
{ id: 1 },
{
id: types.number,
name: types.string,
bankAccount: {
id: types.number,
branch: types.optional.string,
},
},
),
}
Note that we use our types
helper to define types in the result, and the params
helper to define the parameters.
Then, convert the JS Object to GraphQL (string) with graphqlify
:
import { query } from 'typed-graphqlify'
const gqlString = query('getUser', getUserQuery)
console.log(gqlString)
// =>
// query getUser {
// user(id: 1) {
// id
// name
// bankAccount {
// id
// branch
// }
// }
// }
Finally, execute the GraphQL:
import { executeGraphql } from 'some-graphql-request-library'
// We would like to type this!
const result: typeof getUser = await executeGraphql(gqlString)
// As we cast `result` to `typeof getUser`,
// Now, `result` type looks like this:
// interface result {
// user: {
// id: number
// name: string
// bankAccount: {
// id: number
// branch?: string
// }
// }
// }
Currently typed-graphqlify
can convert these GraphQL features:
- Operations
- Query
- Mutation
- Subscription
- Inputs
- Variables
- Parameters
- Data structures
- Nested object query
- Array query
- Scalar types
number
string
boolean
- Enum
- Constant
- Custom type
- Optional types, e.g.)
number | undefined
- Fragments
- Inline Fragments
query getUser {
user {
id
name
isActive
}
}
import { query, types } from 'typed-graphqlify'
query('getUser', {
user: {
id: types.number,
name: types.string,
isActive: types.boolean,
},
})
Or without query name
query {
user {
id
name
isActive
}
}
import { query, types } from 'typed-graphqlify'
query({
user: {
id: types.number,
name: types.string,
isActive: types.boolean,
},
})
Just use mutation
.
mutation updateUserMutation($input: UserInput!) {
updateUser(input: $input) {
id
name
}
}
import { mutation, params } from 'typed-graphqlify'
mutation('updateUserMutation', params({ $input: 'UserInput!' }, {
updateUser: params({ input: '$input' }, {
id: types.number,
name: types.string,
}),
})
Write nested objects just like GraphQL.
query getUser {
user {
id
name
parent {
id
name
grandParent {
id
name
children {
id
name
}
}
}
}
}
import { query, types } from 'typed-graphqlify'
query('getUser', {
user: {
id: types.number,
name: types.string,
parent: {
id: types.number,
name: types.string,
grandParent: {
id: types.number,
name: types.string,
children: {
id: types.number,
name: types.string,
},
},
},
},
})
Just add array to your query. This does not change the result, but TypeScript will be aware the field is an array.
query getUsers {
users(status: 'active') {
id
name
}
}
import { params, query, types } from 'typed-graphqlify'
query('users', {
users: params({ status: 'active' }, [
{
id: types.number,
name: types.string,
},
]),
})
Add types.optional
or optional
helper method to define optional field.
import { optional, query, types } from 'typed-graphqlify'
query('getUser', {
user: {
id: types.number,
name: types.optional.string, // <-- user.name is `string | undefined`
bankAccount: optional({ // <-- user.bankAccount is `{ id: number } | undefined`
id: types.number,
}),
},
}
Use types.constant
method to define constant field.
query getUser {
user {
id
name
__typename # <-- Always `User`
}
}
import { query, types } from 'typed-graphqlify'
query('getUser', {
user: {
id: types.number,
name: types.string,
__typename: types.constant('User'),
},
})
Use types.oneOf
method to define Enum field.
query getUser {
user {
id
name
type # <-- `Student` or `Teacher`
}
}
import { query, types } from 'typed-graphqlify'
enum UserType {
'Student',
'Teacher',
}
query('getUser', {
user: {
id: types.number,
name: types.string,
type: types.oneOf(UserType),
},
})
Note: Currently creating type from array element is not supported in TypeScript. See microsoft/TypeScript#28046
Add other queries at the same level of the other query.
query getFatherAndMother {
father {
id
name
}
mother {
id
name
}
}
import { query, types } from 'typed-graphqlify'
query('getFatherAndMother', {
father: {
id: types.number,
name: types.string,
},
mother: {
id: types.number,
name: types.number,
},
})
Query alias is implemented via a dynamic property.
query getMaleUser {
maleUser: user {
id
name
}
}
import { alias, query, types } from 'typed-graphqlify'
query('getMaleUser', {
[alias('maleUser', 'user')]: {
id: types.number,
name: types.string,
},
}
Use the fragment
helper to create them, and spread the result into places the fragment is used.
query {
user(id: 1) {
...userFragment
}
maleUsers: users(sex: MALE) {
...userFragment
}
}
fragment userFragment on User {
id
name
bankAccount {
...bankAccountFragment
}
}
fragment bankAccountFragment on BankAccount {
id
branch
}
import { alias, fragment, params, query } from 'typed-graphqlify'
const bankAccountFragment = fragment('bankAccountFragment', 'BankAccount', {
id: types.number,
branch: types.string,
})
const userFragment = fragment('userFragment', 'User', {
id: types.number,
name: types.string,
bankAccount: {
...bankAccountFragment,
},
})
query({
user: params({ id: 1 }, {
...userFragment,
}),
[alias('maleUsers', 'users')]: params({ sex: 'MALE' }, {
...userFragment,
}),
}
Use on
helper to write inline fragments.
query getHeroForEpisode {
hero {
id
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
import { on, query, types } from 'typed-graphqlify'
query('getHeroForEpisode', {
hero: {
id: types.number,
...on('Droid', {
primaryFunction: types.string,
}),
...on('Human', {
height: types.number,
}),
},
})
See more examples at src/index.test.ts
There are some GraphQL -> TypeScript convertion tools. The most famous one is Apollo codegen:
https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output
In this section, we will go over why typed-graphqlify
is a good alternative.
Disclaimer: I am not a heavy user of Apollo codegen, so the following points could be wrong. And I totally don't mean disrespect Apollo codegen.
Apollo codegen is a great tool. In addition to generating query interfaces, it does a lot of tasks including downloading schemas, schema validation, fragment spreading, etc.
However, great usability is the tradeoff of complexity.
There are some issues to generate interfaces with Apollo codegen.
I (and maybe everyone) don't know the exact reasons, but Apollo's codebase is too large to find out what the problem is.
On the other hand, typed-graphqlify
is as simple as possible by design, and the logic is quite easy. If some issues happen, we can fix them easily.
Currently Apollo codegen cannot handle multiple schemas.
Although I know this is a kind of edge case, but if we have the same type name on different schemas, which schema is used?
Some graphql frameworks, such as laravel-graphql, cannot print schema as far as I know. I agree that we should avoid to use such frameworks, but there must be situations that we cannot get graphql schema for some reasons.
It is useful to write GraphQL programmatically, although that is an edge case.
Imagine AWS management console:
If you build something like that with GraphQL, you have to build GraphQL dynamically and programmatically.
typed-graphqlify works for such cases without losing type information.
To get started with a development installation of the typed-graphqlify, follow the instructions at our Contribution Guide.
Inspired by