Skip to main content

Overview

The Type Map is the central index for all GraphQL components in your application. It tracks types, directives, and schemas, ensuring they’re properly accessible from their requested scope. The Type Map also provides versioning for cache invalidation.
Inspired by ActiveRecord::Type::TypeMap, the Type Map uses a three-tier index: namespace → base_class → key.

Architecture

The Type Map index has three levels:
# From lib/rails/graphql/type_map.rb
@index = Concurrent::Map.new do |h1, key1|                # Namespaces
  base_class = Concurrent::Map.new do |h2, key2|          # Base classes
    ensure_base_class!(key2)
    h2.fetch_or_store(key2, Concurrent::Map.new)          # Items
  end
  
  h1.fetch_or_store(key1, base_class)
end
1

Namespace

The namespace of the element (e.g., :base, :admin, :public)
2

Base Class

One of :Type, :Directive, or :Schema
3

Key

The unique identifier (symbol or string name)

Versioning

The Type Map maintains a version for cache invalidation:
def version
  @version ||= GraphQL.config.version&.first(8) || SecureRandom.hex(8)
end
Cached resources should check the Type Map version and invalidate if it doesn’t match.

Registration

Postponed Registration

Components are postponed until the index is consulted:
def postpone_registration(object)
  source = caller(3).find { |item| item !~ FILTER_REGISTER_TRACE }
  @pending << [object, source]
end
Registration is delayed because classes may change after inheritance, and those changes affect registration.

Actual Registration

When fetching from the Type Map, pending objects are registered:
# From lib/rails/graphql/type_map.rb
def register(object)
  namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true)
  namespaces << :base if namespaces.empty?
  
  base_class = find_base_class(object)
  ensure_base_class!(base_class)
  
  # Cache the name, key, and alias proc
  object_base = namespaces.first
  object_name = object.gql_name
  object_key = object.to_sym
  alias_proc = -> do
    value = dig(object_base, base_class, object_key)
    value.is_a?(Proc) ? value.call : value
  end
  
  # Register main type object for both key and name
  add(object_base, base_class, object_key, object)
  add(object_base, base_class, object_name, alias_proc)
  
  # Register all aliases
  aliases = object.try(:aliases)
  aliases&.each do |alias_name|
    add(object_base, base_class, alias_name, alias_proc)
  end
  
  # Register in remaining namespaces
  if namespaces.size > 1
    keys_and_names = [object_key, object_name, *aliases]
    namespaces.drop(1).product(keys_and_names) do |(namespace, key_or_name)|
      add(namespace, base_class, key_or_name, alias_proc)
    end
  end
  
  @objects += 1
  object
end
@index[:base][:Type][:string] = Rails::GraphQL::Type::Scalar::StringScalar

Fetching Types

fetch vs fetch!

# Returns nil if not found
Rails::GraphQL.type_map.fetch(:string)
# => Rails::GraphQL::Type::Scalar::StringScalar

Rails::GraphQL.type_map.fetch(:unknown)
# => nil

With Parameters

Rails::GraphQL.type_map.fetch!(
  :string,               # key
  base_class: :Type,     # base_class
  namespace: :base,      # namespace
)
key
symbol | string
required
The type key or name to fetch
base_class
:Type | :Directive | :Schema
default:":Type"
The base class category
namespace
symbol | array
default:":base"
Namespace(s) to search
exclusive
boolean
default:"false"
Skip searching :base namespace
fallback
symbol
Fallback key if primary not found

Aliases

Register alternative names for types:
Rails::GraphQL.type_map.register_alias(:str, :string)
Database adapters use aliases to map database types to GraphQL types (e.g., varcharstring).

Unregistering

Due to Rails reloader, the Type Map can unregister objects:
# From lib/rails/graphql/type_map.rb
def unregister(*objects)
  objects.each do |object|
    namespaces = sanitize_namespaces(namespaces: object.namespaces, exclusive: true)
    namespaces << :base if namespaces.empty?
    base_class = find_base_class(object)
    
    if object.kind != :source
      @index[namespaces.first][base_class][object.to_sym] = nil
      @objects -= 1
    end
    
    return unless object.const_defined?(NESTED_MODULE, false)
    
    nested_mod = object.const_get(NESTED_MODULE, false)
    nested_mod.constants.each { |name| nested_mod.const_get(name, false).unregister! }
    object.send(:remove_const, NESTED_MODULE)
  end
end
Unregistering assigns nil to the outermost item. Aliases still exist but resolve to nil.

Callbacks

Register hooks that trigger when types are added:
# From lib/rails/graphql/type_map.rb
def after_register(name_or_key, base_class: :Type, **xargs, &block)
  item = fetch(name_or_key, prevent_register: true, base_class: base_class, **xargs)
  return block.call(item) unless item.nil?
  
  namespaces = sanitize_namespaces(**xargs)
  callback = ->(n, b, result) do
    return unless b === base_class && (n === :base || namespaces.include?(n))
    block.call(result)
    true
  end
  
  callbacks[name_or_key].unshift(callback)
end
Rails::GraphQL.type_map.after_register(:custom_type) do |type|
  puts "Custom type registered: #{type.name}"
end

# Later, when registered
class CustomType < GraphQL::Type::Object
end
# Output: Custom type registered: CustomType
Callbacks are useful for conditionally adding fields to types that may not exist yet.

Dependencies

The Type Map lazy-loads dependencies:
def add_dependencies(*list, to:)
  @dependencies[to].concat(list.flatten.compact)
end

def load_dependencies!(**xargs)
  sanitize_namespaces(**xargs).reduce(false) do |result, namespace|
    next result if (list = @dependencies[namespace]).empty?
    
    while (src = list.shift)
      src.is_a?(Proc) ? src.call : require(src)
    end
    
    true
  end
end

Seed Dependencies

def seed_dependencies!
  @dependencies[:base] += [
    "#{__dir__}/type/scalar",
    "#{__dir__}/type/object",
    "#{__dir__}/type/interface",
    "#{__dir__}/type/union",
    "#{__dir__}/type/enum",
    "#{__dir__}/type/input",
    
    "#{__dir__}/type/scalar/int_scalar",
    "#{__dir__}/type/scalar/float_scalar",
    "#{__dir__}/type/scalar/string_scalar",
    "#{__dir__}/type/scalar/boolean_scalar",
    "#{__dir__}/type/scalar/id_scalar",
    
    "#{__dir__}/directive/deprecated_directive",
    "#{__dir__}/directive/include_directive",
    "#{__dir__}/directive/skip_directive",
    "#{__dir__}/directive/specified_by_directive",
  ]
end

Reading the Index

Rails::GraphQL.type_map.exist?(:string)
# => true

Rails::GraphQL.type_map.object_exist?(SomeType)
# => true/false

Inspection

View Type Map statistics:
Rails::GraphQL.type_map.inspect
# => #<Rails::GraphQL::TypeMap [index]
#     @namespaces=2
#     @base_classes=3
#     @objects=45
#     @pending=3
#     @dependencies={base: 15, admin: 0}>

Module Namespaces

Associate modules with namespaces:
def associate(namespace, mod)
  @module_namespaces[mod] = namespace
end

def associated_namespace_of(object)
  return if @module_namespaces.empty?
  object.module_parents.find do |mod|
    ns = @module_namespaces[mod]
    break ns unless ns.nil?
  end
end
When objects have no explicit namespace, the Type Map checks their module parents for associations.

Sources

How sources use the Type Map

Global ID

GlobalID uses Type Map for resolution

Build docs developers (and LLMs) love