Understanding the GraphQL Type System

Introduction

GraphQL is a modern solution for facilitating the communication between a front end and a data source. All of the details and capabilities of a GraphQL implementation are laid out in the GraphQL Schema. In order to write a functioning GraphQL schema, you must understand the GraphQL Type System.

In this article, you will learn about GraphQL types: the five built-in scalar types, Enums, the List and Non-Null wrapping types, Object types, and the abstract Interface and Union types that work alongside them. You will review examples for each type and learn how to use them to build a complete GraphQL schema.

Prerequisites

To get the most out of this tutorial, you should have:

Scalar Types

All the data in a GraphQL schema ultimately resolve to various scalar types, which represent primitive values. GraphQL responses can be represented as a tree, and the scalar types are the leaves at the ends of the tree. There can be many levels in a nested response, but the last level will always resolve to a scalar (or Enum) type. GraphQL comes with five built-in scalar types: Int, Float, String, Boolean, and ID.

Int

Int is a signed 32-bit non-fractional numerical value. It is a signed (positive or negative) integer that does not include decimals. The maximum value of a signed 32-bit integer is 2,147,483,647. This is one of the two built-in scalars used for numerical data.

Float

A Float is a signed double-precision fractional value. It is a signed (positive or negative) number that contains a decimal point, such as 1.2. This is the other built-in scalar used for numerical data.

String

A String is a UTF-8 character sequence. The String type is used for any textual data. This can also include data like very large numbers. Most custom scalars will be types of string data.

Boolean

A Boolean is a true or false value.

ID

An ID is a unique identifier. This value is always serialized as a string, even if the ID is numerical. An ID type might be commonly represented with a Universally Unique Identifier (UUID).

Custom Scalars

In addition to these built-in scalars, the scalar keyword can be used to define a custom scalar. You can use custom scalars to create types that have additional server-level validation, such as Date, Time, or Url. Here is an example defining a new Date type:

scalar Date

The server will know how to handle interactions with this new type using the GraphQLScalarType.

Enum Type

The Enum type, also known as an Enumerator type, describes a set of possible values.

Using the Fantasy Game API theme from other tutorials in the series, you might make an enum for the game characters' Job and Species with all the values the system will accept for them. An Enum is defined with the enum keyword, like so:

"The job class of the character."
enum Job {
  FIGHTER
  WIZARD
}

"The species or ancestry of the character."
enum Species {
  HUMAN
  ELF
  DWARF
}

In this way, it is guaranteed that the Job of a character is FIGHTER or WIZARD and can never accidentally be "purple" or some other random string, which could be possible if you used a String type instead of making a custom Enum. Enums are written in all-caps by convention.

Enums can also be used as the accepted values in arguments. For example, you might make a Hand enum to denote whether a weapon is single-handed (like a short sword) or double-handed (like a heavy axe), and use that to determine whether one or two can be equipped:

enum Hand {
  SINGLE
  DOUBLE
}

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
  hand: Hand
}

type Query {
  weapons(hand: Hand = SINGLE): [Weapon]
}

The Hand enum has been declared with SINGLE and DOUBLE as values, and the argument on the weapons field has a default value of SINGLE, meaning if no argument is passed then it will fall back to SINGLE.

Non-Null Type

You might notice that null or undefined, a common type that many languages consider a primitive, is missing from the list of built-in scalars. Null does exist in GraphQL and represents the lack of a value.

All types in GraphQL are nullable by default and therefore null is a valid response for any type. In order to make a value required, it must be converted to a GraphQL Non-Null type with a trailing exclamation point. Non-Null is defined as a type modifier, which are types used to modify the type it is referring to. As an example, String is an optional (or nullable) string, and String! is a required (or Non-Null) string.

List Type

A List type in GraphQL is another type modifier. Any type that is wrapped in square brackets ([]) becomes a List type, which is a collection that defines the type of each item in a list.

As an example, a type defined as [Int] will be a collection of Int types, and [String] will be a collection of String types. Non-Null and List can be used together to make a type both required and defined as a List, such as [String]!.

Object Type

If GraphQL scalar types describe the "leaves" at the end of the hierarchical GraphQL response, then Object types describe the intermediary "branches", and almost everything in a GraphQL schema is a type of Object.

Objects consist of a list of named fields (keys) and the value type that each field will resolve to. Objects are defined with the type keyword. At least one or more fields must be defined, and fields cannot begin with two underscores (__) to avoid conflict with the GraphQL introspection system.

In the GraphQL Fantasy Game API example, you could create a Fighter Object to represent a type of character in a game:

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
}

In this example, the Fighter Object type has been declared, and it has has four named fields:

  • id yields a Non-Null ID type.
  • name yields a Non-Null String type.
  • level yields an Int type.
  • active yields a Non-Null Boolean type.

Above the declaration, you can also add a comment using double quotes, as in this example: "A hero with direct combat ability and strength.". This will appear as the description for the type.

In this example, each field resolves to a scalar type, but Object fields can also resolve to other Object types. For example, you could create a Weapon type, and the GraphQL schema can be set up where the weapon field on the Fighter will resolve to a Weapon Object:

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
}

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
  weapon: Weapon
}

Objects can also be nested into the fields of other Objects.

Root Operation Types

There are three special Objects that serve as entrypoints into a GraphQL schema: Query, Mutation, and Subscription. These are known as Root Operation types and follow all the same rules as any other Object type.

The schema keyword represents the entrypoint into a GraphQL schema. Your root Query, Mutation, and Subcription types will be on the root schema Object:

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

The Query type is required on any GraphQL schema and represents a read request, similar to a REST API GET. The following is an example of a root Query Object that returns a List of Fighter types:

type Query {
  fighters: [Fighter]
}

Mutations represent a write request, which would be analogous to a POST, PUT, or DELETE in a REST API. In the following example, the Mutation has an addFighter field with a named argument (input):

type Mutation {
  addFighter(input: FighterInput): Fighter
}

Finally, a Subscription corresponds to an event stream, which would be used in conjunction with a Websocket in a web app. In the GraphQL Fantasy API, perhaps it could be used for random battle encounters, like so:

type Subscription {
  randomBattle(enemy: Enemy): BattleResult
}

Note that the schema entrypoint is often abstracted away in some GraphQL implementations.

Field Arguments

The fields of a GraphQL Object are essentially functions that return a value, and they can accept arguments like any function. Field arguments are defined by the name of the argument followed by the type. Arguments can be any non-Object type. In this example, the Fighter Object can be filtered by the id field (which resolves to a Non-Null ID type):

type Query {
  fighter(id: ID!): Fighter
}

This particular example is useful for fetching a single item from the data store, but arguments can also be used for filtering, pagination, and other more specific queries.

Interface Type

Like the Object type, the abstract Interface type consists of a list of named fields and their associated value types. Interfaces look like and follow all the same rules as Objects but are used to define a subset of an Object's implementation.

So far in your schema, you have a Fighter Object, but you might also want to make a Wizard, a Healer, and other Objects that will share many of the same fields but have a few differences. In this case, you can use an Interface to define the fields they all have in common, and create Objects that are implementations of the Interface.

In the following example, you could create a BaseCharacter Interface using the interface keyword with all the fields every type of character will possess:

"A hero on a quest."
interface BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job
}

Every character type will have the fields id, name, level, species, and job.

Now, imagine you have a Fighter type and a Wizard type that have these shared fields, but Fighters use a Weapon and Wizards use Spells. You can use the implements keyword to delineate each as a BaseCharacter implementation, which means they must have all the fields from the created Interface:

"A hero with direct combat ability and strength."
type Fighter implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  weapon: Weapon
}

"A hero with a variety of magical powers."
type Wizard implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  spells: [Spell]
}

Fighter and Wizard are both valid implementations of the BaseCharacter Interface because they have the required subset of fields.

Union Type

Another abstract type that can be used with Objects is the Union type. Using the union keyword, you can define a type with a list of Objects that are all valid as responses.

Using the Interfaces created in the previous section, you can create a Character Union that defines a character as a Wizard OR a Fighter:

union Character = Wizard | Fighter

The equal character (=) sets the definition, and the pipe character (|) functions as the OR statement. Note that a Union must consist of Objects or Interfaces. Scalar types are not valid on a Union.

Now if you query for a list of characters, it could use the Character Union and return all Wizard and Fighter types.

Conclusion

In this tutorial, you learned about many of the types that define the GraphQL type system. The most fundamental types are the scalar types, which are the values that act as the leaves on the schema tree, and consist of Int, Float, String, Boolean, ID, and any custom scalar that a GraphQL implementation decides to create. Enums are lists of valid constant values that can be used when you need more control over a response than simply declaring it as a String, and are also leaves on the schema tree. List and Non-Null types are known as type modifiers, or wrapping types, and they can define other types as collections or required, respectively. Objects are the branches of the schema tree, and almost everything in a GraphQL schema is a type of Object, including the query, mutation, and subscription entrypoints. Interface and Union types are abstract types that can be helpful in defining Objects.

For further learning, you can practice creating and modifying a GraphQL schema by reading the How to Set Up a GraphQL API Server in Node.js tutorial to have a working GraphQL server environment.

Comments