TechLead
Lesson 2 of 20
6 min read
GraphQL

Schema and Type System

Master the GraphQL type system including scalar types, object types, enums, interfaces, unions, and input types

The GraphQL Type System

The type system is the foundation of every GraphQL API. It defines what data can be queried, what shape it takes, and what operations are available. Every GraphQL service defines a set of types that completely describe the set of possible data you can query on that service.

When queries come in, they are validated and executed against the schema. This strong typing enables powerful developer tooling like autocompletion, documentation generation, and compile-time error checking.

Scalar Types

Scalars are the leaf nodes of a GraphQL query — they resolve to concrete data. GraphQL comes with five built-in scalar types:

Type Description Example
IntSigned 32-bit integer42
FloatDouble-precision floating point3.14
StringUTF-8 character sequence"hello"
Booleantrue or falsetrue
IDUnique identifier (serialized as String)"abc-123"

Custom Scalar Types

You can define custom scalars for domain-specific data like dates, URLs, or email addresses:

# Schema definition
scalar DateTime
scalar Email
scalar URL

type Event {
  id: ID!
  name: String!
  date: DateTime!
  website: URL
  organizer: Email!
}
import { GraphQLScalarType, Kind } from 'graphql';

const DateTimeScalar = new GraphQLScalarType({
  name: 'DateTime',
  description: 'A date-time string in ISO 8601 format',
  serialize(value: unknown): string {
    // Sent to the client
    if (value instanceof Date) {
      return value.toISOString();
    }
    throw new Error('DateTime cannot represent non-Date type');
  },
  parseValue(value: unknown): Date {
    // Received from the client as a variable
    if (typeof value === 'string') {
      const date = new Date(value);
      if (isNaN(date.getTime())) {
        throw new Error('DateTime cannot represent invalid date string');
      }
      return date;
    }
    throw new Error('DateTime must be a string');
  },
  parseLiteral(ast): Date {
    // Received from the client inline in the query
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    throw new Error('DateTime must be a string');
  },
});

Object Types

Object types are the most common type in a GraphQL schema. They represent a collection of fields, where each field has its own type:

type User {
  id: ID!
  username: String!
  email: String!
  bio: String
  avatarUrl: URL
  posts: [Post!]!
  followers: [User!]!
  followersCount: Int!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  body: String!
  excerpt: String
  author: User!
  tags: [Tag!]!
  comments: [Comment!]!
  likesCount: Int!
  isPublished: Boolean!
  publishedAt: DateTime
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Tag {
  id: ID!
  name: String!
  slug: String!
  posts: [Post!]!
}

Non-Null and List Modifiers

  • String — nullable string (can be null)
  • String! — non-null string (guaranteed to have a value)
  • [String] — nullable list of nullable strings
  • [String!] — nullable list of non-null strings
  • [String!]! — non-null list of non-null strings (always returns an array, items are never null)

Enum Types

Enums define a type that is restricted to a particular set of allowed values. They are useful for status fields, roles, categories, and any field with a finite set of options:

enum UserRole {
  ADMIN
  MODERATOR
  EDITOR
  VIEWER
}

enum PostStatus {
  DRAFT
  REVIEW
  PUBLISHED
  ARCHIVED
}

enum SortOrder {
  ASC
  DESC
}

type User {
  id: ID!
  name: String!
  role: UserRole!
}

type Query {
  posts(status: PostStatus, sortBy: SortOrder): [Post!]!
}

Interface Types

Interfaces define a set of fields that multiple types must include. They enable polymorphism, allowing you to query for a common set of fields regardless of the concrete type:

interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

interface Likeable {
  likesCount: Int!
  isLikedByViewer: Boolean!
}

type Post implements Node & Likeable {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  likesCount: Int!
  isLikedByViewer: Boolean!
  title: String!
  body: String!
  author: User!
}

type Comment implements Node & Likeable {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  likesCount: Int!
  isLikedByViewer: Boolean!
  text: String!
  author: User!
  post: Post!
}

Union Types

Unions are similar to interfaces but do not require any shared fields. They represent a value that could be one of several different object types. Unions are ideal for search results, activity feeds, and polymorphic relationships:

union SearchResult = User | Post | Tag | Comment

type Query {
  search(query: String!): [SearchResult!]!
}

# Querying unions requires inline fragments
query SearchExample {
  search(query: "graphql") {
    ... on User {
      id
      username
      avatarUrl
    }
    ... on Post {
      id
      title
      excerpt
      author { username }
    }
    ... on Tag {
      id
      name
      slug
    }
    ... on Comment {
      id
      text
      author { username }
    }
  }
}

Input Types

Input types are special object types used for mutation arguments. They cannot have fields that resolve to other object types — only scalars, enums, and other input types. This separation ensures clarity between what clients send and what the server returns:

input CreateUserInput {
  username: String!
  email: String!
  bio: String
  role: UserRole = VIEWER
}

input UpdateUserInput {
  username: String
  email: String
  bio: String
  role: UserRole
}

input PostFilterInput {
  status: PostStatus
  authorId: ID
  tags: [String!]
  search: String
  dateRange: DateRangeInput
}

input DateRangeInput {
  from: DateTime!
  to: DateTime!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

type Query {
  posts(filter: PostFilterInput, limit: Int = 20, offset: Int = 0): [Post!]!
}

Input Type Best Practices

  • Use Input suffix: Name your input types with an Input suffix for clarity (e.g., CreateUserInput)
  • Separate create/update inputs: Create inputs have required fields; update inputs have all optional fields
  • Wrap in a single argument: Use input: CreateUserInput! instead of many individual arguments
  • Default values: Provide sensible defaults like role: UserRole = VIEWER

Type System in TypeScript Resolvers

When building resolvers in TypeScript, you want your types to match the schema. Here is how to define TypeScript types that mirror your GraphQL schema:

// Types matching the GraphQL schema
interface User {
  id: string;
  username: string;
  email: string;
  bio: string | null;
  role: 'ADMIN' | 'MODERATOR' | 'EDITOR' | 'VIEWER';
  createdAt: Date;
  updatedAt: Date;
}

interface Post {
  id: string;
  title: string;
  body: string;
  excerpt: string | null;
  authorId: string; // foreign key, resolved to User by resolver
  isPublished: boolean;
  publishedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
}

// Resolver type definitions
import { GraphQLResolveInfo } from 'graphql';

type Context = {
  db: Database;
  userId: string | null;
};

type Resolvers = {
  Query: {
    user: (parent: unknown, args: { id: string }, ctx: Context, info: GraphQLResolveInfo) => Promise<User | null>;
    posts: (parent: unknown, args: { filter?: PostFilter; limit?: number; offset?: number }, ctx: Context) => Promise<Post[]>;
  };
  User: {
    posts: (parent: User, args: unknown, ctx: Context) => Promise<Post[]>;
    followersCount: (parent: User, args: unknown, ctx: Context) => Promise<number>;
  };
  Post: {
    author: (parent: Post, args: unknown, ctx: Context) => Promise<User>;
  };
};

Key Takeaways

  • The type system is the contract — it defines every possible interaction with your API
  • Use non-null wisely — only mark fields as non-null if you can always guarantee a value
  • Interfaces share common fields — use them when types share behavior
  • Unions handle polymorphism — use them when types do not share fields
  • Input types separate concerns — keep input shapes distinct from output types

Continue Learning