Utilize GraphQL Code Generator for newType in Typescript
Recently we started building our new features with GraphQL and Typescript. The reason why we chose to use type system is because we suffered from the runtime error in our legacy code a lot (the notorious undefined is not a function
😅).
After digging into how Typescript works, we figured out that it can achieve the same characteristic of newType similar to Haskell. newType
introduces the benefit of not mixing different fields with the same type. For instance, both your email and phone number can be represented by string
, but they have quite different meaning. If we only use type alias for them, then Typescript type-check cannot figure out they are different. Here is a more thorough explanation of newType in Typescript.
But how do we combine newType in Typescript with GraphQL schema?
When using GraphQL code generator, normally we will get something similar to the following generated schema from our GraphQL server:
export type Scalars = { | |
ID: string; | |
String: string; | |
Boolean: boolean; | |
Int: number; | |
Float: number; | |
}; | |
export type Node = { | |
id: Scalars["ID"]; | |
}; | |
export type Person = Node & { | |
id: Scalars["ID"]; | |
name: Scalars["String"]; | |
email: Scalars["String"]; | |
age: Scalars["Int"]; | |
}; |
As we know, although name and email are both string
, they have quite different concept. In this case, if you accidentally pass a person’s name to a function that only accepts email, type checker won’t yell at it. Now we would like to make our app more type-safe by adding newType into it!
From the backend part (i.e., GraphQL server), you will need to add a new type, say Email
. After re-generating your schema by code generator, you will get the following result:
export type Scalars = { | |
ID: string; | |
String: string; | |
Boolean: boolean; | |
Int: number; | |
Float: number; | |
Email: any; | |
}; | |
export type Node = { | |
id: Scalars["ID"]; | |
}; | |
export type Person = Node & { | |
id: Scalars["ID"]; | |
name: Scalars["String"]; | |
email: Scalars["Email"]; | |
age: Scalars["Int"]; | |
}; |
Now we have a new Scalar type: Email
. However, its type becomes any
, which is not type-safe at all :(
Luckily, we can override the built-in scalars and custom GraphQL scalars to a custom type by GraphQL code generator 🎉. Simply adding the magic newType declaration for it in your .yml
file.
overwrite: true | |
schema: 'localhost:8000/graphql' | |
documents: '**/*.graphql' | |
generates: | |
genaratedSchema.tsx: | |
plugins: | |
- add: '// THIS IS A GENERATED FILE' | |
- typescript | |
- typescript-operations | |
config: | |
scalars: | |
Email: 'string & { readonly Email: unique symbol }' |
The above example makes Email a newType, i.e., it is not just string
anymore. And re-generating your schema will get the following:
export type Scalars = { | |
ID: string; | |
String: string; | |
Boolean: boolean; | |
Int: number; | |
Float: number; | |
Email: string & { readonly Email: unique symbol }; | |
}; | |
export type Node = { | |
id: Scalars["ID"]; | |
}; | |
export type Person = Node & { | |
id: Scalars["ID"]; | |
name: Scalars["String"]; | |
email: Scalars["Email"]; | |
age: Scalars["Int"]; | |
}; |
Now, when you accidentally pass any string that is not with type Email
, type checker will tell you! Just like the following:
const logEmail = (email: Email) => console.log(email); | |
logEmail("123"); | |
/* | |
^^^^^^^^^^^^^^^ | |
Argument of type '"123"' is not assignable to parameter of type 'string & { readonly Email: unique symbol; }'. | |
Type '"123"' is not assignable to type '{ readonly Email: unique symbol; }'. | |
*/ |
Now your app is more type-safe :)
This article is originally published on my Medium.