Skip to content

GraphQL - Type System

GraphQL’s strong type system is a cornerstone feature. It defines the capabilities of an API, ensures predictability, enables powerful developer tooling, and catches errors early. Every GraphQL service defines a set of types which completely describe the set of possible data you can query on that service.

GraphQL types can be broadly categorized as:

  • Scalar Types: Represent primitive leaf values.
  • Object Types: Represent structured data with fields.
  • Input Object Types: Special objects used as arguments for mutations/queries.
  • Enum Types: Represent a fixed set of possible string values.
  • Interface Types: Define a contract of fields that implementing Object types must include.
  • Union Types: Represent a value that could be one of several Object types.
  • Root Operation Types: Query, Mutation, Subscription defining API entry points.

Scalars represent single, indivisible values. GraphQL comes with built-in default scalar types:

  • Int: A signed 32‐bit integer.
  • Float: A signed double‐precision floating-point value.
  • String: A UTF‐8 character sequence.
  • Boolean: true or false.
  • ID: Represents a unique identifier, often used for fetching objects or as keys for caching. Serialized as a String, but not intended to be human-readable.

You can also define Custom Scalar Types for specific data formats (e.g., Date, JSON, UUID) if the built-in ones aren’t sufficient. This requires defining both the schema declaration and server-side logic for serialization, deserialization, and validation.

# Built-in Scalar Usage
scalar Date # Custom scalar declaration
type Product {
id: ID!
name: String!
price: Float
inStock: Boolean
releaseDate: Date # Using custom scalar
}

Object types are the most common in GraphQL. They represent a collection of named fields, where each field has its own type (which could be a scalar, enum, or another object type, allowing for nesting).

# Object Type Definition
type Student {
id: ID!
firstName: String!
lastName: String
age: Int
college: College # Field linking to another Object type
}

The Query type is a special Object type that defines all the possible entry points for read operations on your API. Every GraphQL service must have a Query type, though it might be empty.

# Query Type Definition
type Query {
greeting: String # Simple query returning a scalar
students: [Student] # Query returning a list of Student objects
studentById(id: ID!): Student # Query taking an argument, returning one Student
}

Similar to Query, the Mutation type is a special Object type defining the entry points for write operations (create, update, delete).

# Mutation Type Definition
input CreateStudentInput { # Often use Input Objects for mutation args
firstName: String!
collegeId: ID!
}
type Mutation {
createStudent(input: CreateStudentInput!): Student # Creates a student, returns the new student
deleteStudent(id: ID!): Boolean # Deletes a student, returns success status
}

The Subscription type defines entry points for establishing real-time connections with the server, allowing clients to receive updates pushed from the server when specific events occur.

# Subscription Type Definition (Conceptual)
type Subscription {
newNotification(userId: ID!): Notification # Subscribe to notifications for a user
postUpdated(postId: ID!): Post # Subscribe to updates for a specific post
}

Enumeration types restrict a field’s value to one of a predefined set of allowed string values. Useful for representing categories, statuses, etc.

# Enum Definition
enum CourseLevel {
BEGINNER
INTERMEDIATE
ADVANCED
}
type Course {
id: ID!
title: String!
level: CourseLevel # Field using the Enum type
}

You can declare that a field will return a list (array) of a certain type by wrapping the type in square brackets [].

# List Type Usage
type Query {
studentNames: [String] # Returns a list of strings
allCourses: [Course] # Returns a list of Course objects
}

By default, any field in GraphQL can return null. To guarantee that a field will always return a value (or trigger an error if it can’t), you append an exclamation mark ! to the type. This applies to arguments as well.

# Non-Nullable Usage
type User {
id: ID! # ID will never be null
username: String! # Username will never be null
bio: String # Bio can be null (optional)
}
type Query {
userById(id: ID!): User # id argument is required, return type can be null
searchUsers(term: String!): [User!]! # Requires term, returns a non-null list
# where each User in the list is also non-null
}

Understanding combinations:

  • String: Can be a string or null.
  • String!: Must be a string, cannot be null.
  • [String]: Can be a list of strings, an empty list, or null.
  • [String]!: Must be a list of strings or an empty list, cannot be null itself. Items within the list can be null.
  • [String!]: Can be a list of non-null strings, an empty list, or null. The list itself can be null.
  • [String!]!: Must be a list (cannot be null), and every item within the list must be a non-null string.

Interfaces: Define a set of fields that implementing Object types must adhere to. Useful for polymorphism when different types share common fields (e.g., Character interface implemented by Human and Droid).

Unions: Define a type that can resolve to one of several distinct Object types, which don’t necessarily share fields. Useful for returning mixed search results (e.g., SearchResult could be a User, Post, or Comment).

These abstract types add more flexibility for modeling complex domains and require special handling in queries (... on TypeName fragments) and resolvers (__resolveType).