Build Typed GraphQL Query in TypeScript. 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 API, 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 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 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 { graphqlify, types } from 'typed-graphqlify'
const getUserQuery = {
getUser: {
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.
Then, convert the JS Object to GraphQL (string) with graphqlify
:
const gqlString = graphqlify.query(getUserQuery)
console.log(gqlString)
// =>
// query getUser {
// user(id: 1) {
// id
// name
// bankAccount {
// id
// branch
// }
// }
// }
Finally, execute the GraphQL:
// GraphQLData is a type helper which returns one level down
import { GraphQLData } from 'typed-graphqlify'
import { executeGraphql } from 'some-graphql-request-library'
// We would like to type this!
const result: GraphQLData<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
// }
// }
// }
- Nested Query
- Array Query
- Input variables, parameters
- Query and Mutation
- Optional types
query getUser {
user {
id
name
isActive
}
}
graphqlify.query({
getUser: {
user: {
id: types.number,
name: types.string,
isActive: types.boolean,
},
},
})
Change the first argument of graphqlify
to mutation
.
mutation updateUser($input: UserInput!) {
updateUser(input: $input) {
id
name
}
}
graphqlify.mutation({
__params: { $input: 'UserInput!' },
updateUser: {
__params: { input: '$input' },
id: types.number,
name: types.string,
},
})
Write nested object just like GraphQL.
query getUser {
user {
id
name
parent {
id
name
grandParent {
id
name
children {
id
name
}
}
}
}
}
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 of compile, but TypeScript can aware the field is array.
query getUsers {
users(status: 'active') {
id
name
}
}
graphqlify.query({
getUsers: {
users: [
{
__params: { status: 'active' },
id: types.number,
name: types.string,
},
],
},
})
Add types.optional
or optional
helper method to define optional field.
import { types, optional } from 'typed-graphqlify'
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`
}
}
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`
}
}
enum UserType {
'Student',
'Teacher',
}
graphqlify.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
}
}
graphqlify.query({
getFatherAndMother: {
father: {
id: types.number,
name: types.string,
},
mother: {
id: types.number,
name: 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 would like to explain why typed-graphqlify
comes.
Disclaimer: I am not a heavy user of Apollo codegen, so the following points could be wrong. And I totally don't disrespect Apollo codegen.
Apollo codegen is a great tool. In addition to generating query interfaces, it does a lot of tasks including downloading schema, 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 is the problem.
On the other hand, typed-graphqlify
is as simple as possible tool by design, and the logic is quite easy. So I think 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 taken?
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.
- Optional support
- Enum support
Inspired by