Skip to content

Overview of type system

Hugo Tiburtino edited this page Jan 2, 2023 · 1 revision

We use typescript in this project. This gives us the potential to check the correctness of our code with types. This means that the typescript compiler can check whether the code makes sense and throws an error in case we have made an error (this is referred to as "type safety"). For example it can check whether our resolver functions return correct data, whether we only use data provided by the services or whether we have provided all resolver functions which are necessary.

Overview

  • Payload<"serlo", "getAlias"> – the payload of the endpoint getAlias() in the data source model serlo
  • GraphQL types like User, AbstractUuid, Article, etc. are defined in ~/types with yarn regenerate-types.
  • Model<"User"> – the model type for the GraphQL type User
  • resolver type helpers:
    • Resolvers in ~/types can be used for all resolvers objects
    • Queries<"uuid"> – type to implement the query endpoint uuid
    • Mutations<"thread"> – type to implement all resolver functions in the namespace thread
    • TypeResolvers<Comment> – type to implement all resolver functions not covered by the model type
    • InterfaceResolvers<"AbstractUuid"> – type to implement the resolver functions for a GraphQL interface or union type

Model types

Let's take the following GraphQL schema:

type ArticleRevision {
  title: String!
  content: String!
  threads: ThreadConnection!
  author: User
}

type User { ... }
type ThreadConnection!

A possible response would be therefore

{
  "title": "Theorem of Pythagoras",
  "content": "Hello World, Lorum Ipsum...",
  "author": {
    "username": "FooBar",
    ...
  },
  "threads": { ... }
}

However this is not a good way to represent internally a ArticleRevision for the following reasons:

  • Caching / Overhead: Since the user and threads object is nested into the revision object it is hard to update a cached version of a revision since each update of an user / a thread needs to reflect to an update of the revision as well. Also the user is stored many times in the cache.
  • Performance: To create such an object we will need either two SQL queries against the revision and the user table or we need a join those tables. This extra effort is also necessary when only the title or content of the revision is requested.

A better way is to design an object which holds all the necessary information about an article revision so that we can dynamically create all the necessary properties. Here we also avoid the nesting of objects. Such a model might be:

interface ArticleRevisionModel {
  id: number // With this we query the comments attached to the revision
  authorId: number // With this we can request the user object
  title: string
  content: string
}

We call such objects which describe a GraphQL type model types. You can access them by the type helper Model<...> which takes the name of the type as its argument. In the above example the model of an article revision is therefore Model<"ArticleRevision">:

type Model<"ArticleRevision"> = {
  id: number
  authorId: number
  title: string
  ...
}

Model<...> can also resolve the model types of GraphQL unions and interfaces. Internally it is resolved to the TypeScript union of all model types whose GraphQL type implement the interface or are in the GraphQL union. For example we have:

type Model<"AbstractUuid"> = Model<"Article"> | Model<"ArticleRevision"> | Model<"User"> ...

Sidenote: The parent type of a resolver function will always be the model type of the corresponding GraphQL type. Also the return type of a resolver function needs to be always a model type. Take for example the author() resolver function for ArticleRevision. The parent will be Model<"ArticleRevision"> and it should return an object of type Model<"User">:

export resolvers: Resolvers = {
  ArticleRevision: {
    async author(parent /* is of type Model<"ArticleRevision"> */) {
      ...
      return result // must be of type Model<"User">
    }
  }
}

Sidenote: The most important service we have is the database layer and since we have control over it we tend to design it in a way that the payload of an endpoint equals the model type of the requested object. This is the reason why you will use Model<...> more often than Payload<...>. When you want to add a new functionality start with designing the model type and from it you can design the endpoint of the database layer.

Registering new model types

In order to register new model types you need to include them to the Models interface in ~/model/types.ts.

Resolver types

Resolver functions are used to dynamically compute properties which are missing in the model type of a GraphQL type. Resolvers are defined in ~/schema. We have type helpers which will help you to check whether your resolver functions are right.

Clone this wiki locally