apollographql / apollographql/apollo-tooling
Support JSDoc to Generate Types for GQL Scalars
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 460
- PR merge metrics
- No merged PRs in 30d
Description
# Support JSDoc to Generate Types for GQL Scalars
Note: This is a brief summary of Post: [typed-scalars](https://github.com/nalchevanidze/iris/blob/main/examples/app/examples-iris-app.cabal).
## Motivation
One of the fundamental strengths of GraphQL is that we have control over the depth of the structure by selecting fields in your query. This limitation stops graph databases and recursive data types with independent resolvers running into the loop.
Nevertheless, this design can be obstructive in some cases. The first example is introspection queries, where the client has to query the fields nine levels deep to cover the most useful types.
```graphql
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
```
Hypothetically, we could create a schema with type `[[[[[User]!]!]!]!` where we could break clients. Since this scenario is doubtful, it was never a big issue in GraphQL. However, there are other recursive types (`Tree Types`), where this issue can be challenging. They can have hundreds or even thousands of nesting levels before reaching the leaf nodes, which sometimes makes them impossible to query. Let us consider one instance: the RichText. We want to create `RichText` in our `WebApp` where we use GraphQL `BFF`.
```graphql
enum RichTextNodeType {
Label
Paragraph
Image
}
type RichTextNode {
type: RichTextNodeType!
src: String
text: String
children: [RichTextNode!]
}
```
For this case, the solution presented above (see TypeRef) is no longer applicable, as it can have hundreds of nesting levels depending on the content.
## Solution in GraphQL
The straightforward solution to this problem is to represent `RichText` by custom scalar and map it to the specific type. However, this works well when server and client are packages in the same Monorepo and use the same language. If we target third-party clients, we need to define the library "@types/rich-text" and publish it on `npm` for them.
```yaml
// apollo.config.yaml
config:
scalars:
RichTextNode: import('@types/rich-text').RichTextNode
```
However, this approach has the following problems:
- What if we want to target different languages (Java, TS, Flow, Elm ... )? Should we manually provide a type definitions library for each particular language? Even if we do that, we have to maintain each of them to introduce updates in the data types.
- are we sure we have the correct version of the type definitions for the API?
- The validity of the values is not checked by GraphQL automatically, but the developer has to check it manually.
- Never the less, in Apollo Codegen, we have to map library types to scalar types by hand.
A general solution in GraphQL is typed scalars (which we have in [Iris](https://github.com/nalchevanidze/iris) as `data` types). A typed scalar will represent JSON values without getting its dedicated resolvers. GraphQL compiler will only check if the values match type definitions and will not automatically resolve their fields. That way, we would not run into the loop but still have type safety guaranteed by the compiler.
One attempt of solving this problem in GraphQL is to provide type annotations with `JSDoc` in the scalar description, where a type generator could parse annotations and generate corresponding types. In addition, a server with the directive `@JSDoc` could use these annotations to validate scalar (inputs/outputs) values.
```graphql
enum RichTextNodeType = {
Label
Paragraph
Image
}
"""
@type {{
type: RichTextNodeType,
src: ?string,
text: ?string,
children: ?RichTextNode[]
}}
"""
scalar @JSDoc RichTextNode
```
```ts
// __generated__/globalTypes.ts
export type RichTextNodeType = "Label" | "Paragraph" | "Image"
export type RichTextNode = {
type: RichTextNodeType,
src: string | undefined,
text: string | undefined,
children: RichTextNode[] | undefined
}
```
Contributor guide
Research direction
Use the scalar examples and expected __generated__/globalTypes.ts output as the initial specification; done means JSDoc scalar annotations can produce corresponding TypeScript types. The issue names no implementation entry point or tests, so the code-generation path and validation scope need maintainer clarification.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100