This is an experimental and opinionated code generator for generating TypeScript code for GraphQL operations and fragments.
- Types: Generates types for operations, variables, fragments, input types and enums
- Operations: Optionally generates fully valid operations with inlined fragments
- Performance: Fast initial execution (at least 100x faster than graphql-codegen)
- Readability: The generated types are easy to read and understand
- Stateful: The class keeps track of already generated types, allowing it to update only types that need to be rebuilt
- TypeDoc: Adds comments for fragments, operations and fields (description from schema)
- Output: Option to generate a single
.ts
file or separate.js
and.d.ts
files
This is still very much beta and experimental. While there are a lot of tests that cover both basic queries and edge cases, I'm sure there are plenty that I missed. Feel free to report anything you find, ideally by creating a pull request that implements a test case that shows the buggy output.
To just generate types use the static generateOnce
method:
import { Generator } from 'graphql-typescript-deluxe'
import { parse } from 'graphql'
import { loadSchemaSync } from '@graphql-tools/load'
const SCHEMA = `
type Query {
ping: Boolean!
}
`
const DOC = `
query myQuery {
ping
}
`
// Contains the compiled TS types as a string.
const schema = loadSchemaSync(SCHEMA, { loaders: [] })
const result = Generator.generateOnce(schema, DOC)
More interesting, especially in a local dev enviroment, is the stateful usage:
import { Generator } from 'graphql-typescript-deluxe'
import { parse } from 'graphql'
import { loadSchemaSync } from '@graphql-tools/load'
const SCHEMA = `
type Image {
url: String!
alt: String
}
type Query {
getRandomImage: Image!
ping: Boolean!
}
`
const DOC = `
query myQuery {
getRandomImage {
...image
}
}`
const FRAGMENT = `
fragment myImage on Image {
url
}`
const schema = loadSchemaSync(SCHEMA, { loaders: [] })
const generator = new Generator(schema)
// Add the documents.
generator.add([
{
documentNode: parse(DOC),
filePath: './somewhere/in/repo/myQuery.graphql',
},
{
documentNode: parse(FRAGMENT),
filePath: './somewhere/in/repo/fragment.image.graphql',
},
])
// Build the initial output.
console.log(generator.build().getOperations().getSource())
// Add a new one.
generator.add({
documentNode: parse('query myNewQuery { ping }'),
filePath: './somewhere/in/repo/myNewQuery.graphql',
})
// Will only generate code for the new query and reuse all existing code.
console.log(generator.build().getOperations().getSource())
// Update a previously added document.
generator.update({
documentNode: parse('fragment myImage on Image { url, alt }'),
filePath: './somewhere/in/repo/fragment.image.graphql',
})
// Will update both the fragment type *and* the existing query type (since it depends on it).
console.log(generator.build().getOperations().getSource())
Mostly performance: On some of my projects with large schemas and tons of operations, running graphql-codegen takes up to 10s, other team members with less powerful machines report even higher numbers, sometimes up to one minute.
In addition, I was never truly happy with the output, no matter what options I tried. The output also tends to be extremely verbose and large, affecting LSP and IDE performance.
To test the performance I've used a quite large project (~ 15k lines of schema, with 135 fragments and 173 operations) as reference. On average, this generator only takes around 35ms to produce the string with all TypeScript code. Compared to graphql-codegen, which averages around 6000ms for me for the same set of operations/fragments.
The output is very opinionated and the available options to customise the output are fairly limited. I am open to adding new options, as long as they don't negatively affect performance.
A type is generated for every fragment and this type is also used whenever possible.
query fragmentInterface {
getRandomEntity {
...entity
}
}
fragment entity on Entity {
id
}
export type FragmentInterfaceQuery = {
getRandomEntity: EntityFragment | null
}
export type EntityFragment = {
id: string
}
Sometimes it's not possible to use fragment types in an intersection:
query fieldMergingNested {
entityById(id: "1", entityType: NODE) {
...nodeArticleOne
...nodeArticleTwo
}
}
fragment nodeArticleOne on NodeArticle {
title
categories {
label
}
}
fragment nodeArticleTwo on NodeArticle {
tags
categories {
url
}
}
Here both fragments define a nested array field (categories), but pick different fields inside this array.
If we were to intersect both types:
type NodeArticleOneFragment = {
title: string
categories: Array<{
label: string
}> | null
}
type NodeArticleTwoFragment = {
tags: Array<string | null> | null
categories: Array<{
url: string | null
}> | null
}
type Intersected = NodeArticleOneFragment & NodeArticleTwoFragment
Then the type of categories
would be:
const categories:
| ({
label: string
}[] &
{
url: string | null
}[])
| null
So, either an array of { label: string }
or an array of { url: string }
.
In such a case where properties that would result in an invalid intersection are inlined, while still referencing the fragment type by omitting the inlined properties:
export type FieldMergingNestedQuery = {
entityById:
| object
| ((Omit<NodeArticleOneFragment, 'categories'> &
Omit<NodeArticleTwoFragment, 'categories'>) & {
categories: Array<{
label: string
url: string | null
}> | null
})
| null
}
The final resolved type in the IDE will be:
const entityById:
| object
| {
title: string
tags: Array<string | null> | null
categories: Array<{
label: string
url: string | null
}> | null
}
By default a string literal type is generated for every type and an additional union of these for every GraphQL interface or union type.
interface Entity {
id: String!
}
interface Node implements Entity {
id: String!
title: String!
}
type NodePage implements Entity & Node {
id: String!
title: String!
body: String
}
type NodeArticle implements Entity & Node {
id: String!
title: String!
date: String!
body: String!
}
type Image implements Entity {
id: String!
title: String!
imageUrl: String!
}
type Document implements Entity {
id: String!
title: String!
videoUrl: String!
}
union SearchResult = NodeArticle | Document
This will generate the following:
// Types
type NodePage = 'NodePage'
type NodeArticle = 'NodeArticle'
type Image = 'Image'
type Document = 'Document'
// Interfaces
export type Entity = NodePage | NodeArticle | Image | Document
export type Node = NodePage | NodeArticle
// Unions
export type SearchResult = NodeArticle | Document
When using this query for example:
query getRandomEntity {
getRandomEntity {
__typename
... on NodePage {
body
}
}
}
These types are referenced for every __typename
field:
export type GetRandomEntity = {
getRandomEntity:
| { __typename: Exclude<Entity, NodePage> }
| { __typename: NodePage; body: string | null }
| null
}
This prevents generating identical object shapes where only the __typename
changes, which can very quickly bloat the TypeScript code, making it difficult
to make sense of the types. The type above is identical to this one:
export type GetRandomEntity = {
getRandomEntity:
| { __typename: 'Document' }
| { __typename: 'Image' }
| { __typename: 'NodeArticle' }
| { __typename: 'User' }
| { __typename: 'Comment' }
| { __typename: 'NodePage'; body: string | null }
| null
}
By default, enums are exported as a const
(vs. a TypeScript enum
) and
exported as a string literal union type with the same name.
enum ContactMethod {
PHONE
MAIL
}
export const ContactMethod = {
/* Contact via phone. */
PHONE: 'PHONE',
/* Contact via email. */
MAIL: 'MAIL',
} as const
export type ContactMethod = (typeof ContactMethod)[keyof typeof ContactMethod]
This is the output when calling result.getOperations()
, which defaults to
generating a .ts
file. It's also possible to create separate .js
and .d.ts
files:
const output = generator.build()
const js = output.getOperations('js').getSource())
const dts = output.getOperations('d.ts').getSource())
Which produces this:
export const ContactMethod = Object.freeze({
/* Contact via phone. */
PHONE: 'PHONE',
/* Contact via email. */
MAIL: 'MAIL',
})
export declare const ContactMethod: {
/* Contact via phone. */
PHONE: 'PHONE'
/* Contact via email. */
MAIL: 'MAIL'
}
export type ContactMethod = (typeof ContactMethod)[keyof typeof ContactMethod]
No options are required. The defaults are "sane" and picked to produce the smallest and fastest output possible.
Check out all options.