Variable Hoisting
Overview
Variable hoisting is the process of lifting field arguments to the GraphQL operation level as variables. Instead of embedding argument values directly in the query string, hoisting extracts them into a separate variables object that's passed alongside the query.
Why Hoisting?
Hoisting arguments to variables provides several important benefits:
- Better query caching - Same query structure with different variable values allows GraphQL servers to cache and reuse parsed queries
- Automatic custom scalar encoding/decoding - Graffle can automatically encode and decode custom scalars when they flow through variables
- Alignment with GraphQL best practices - Variables are the recommended way to parameterize GraphQL queries
- Separation of query structure from data - Keeps the query structure stable while values change
Example
Without hoisting, arguments are embedded directly:
query {
pokemonByName(name: "Pikachu") {
name
hp
}
}
With hoisting, arguments become variables:
query($name: String!) {
pokemonByName(name: $name) {
name
hp
}
}
# Variables: { "name": "Pikachu" }
Two Approaches
Graffle supports two ways to hoist arguments to variables: manual hoisting and automatic hoisting.
Manual Hoisting
Manual hoisting allows you to explicitly mark which arguments should become variables using the $
sentinel.
Basic Usage
import { $ } from 'graffle'
const doc = Graffle.gql({
query: {
getPokemon: {
pokemonByName: {
$: { name: $('pokemonName').required() },
name: true,
type: true,
},
},
},
})
Generates:
query getPokemon($pokemonName: String!) {
pokemonByName(name: $pokemonName) {
name
type
}
}
Variable Modifiers
The object syntax provides additional control over manually hoisted variables:
import { $ } from 'graffle'
// Required variable
$('name').required()
// Optional variable
$('name').optional()
// Variable with default value
$.default('Ash')
// Anonymous variable (name inferred from argument)
$.required()
Schema-Less Mode Type Hints
When using manual hoisting without a generated schema, provide explicit type hints:
import { $ } from 'graffle'
const doc = Graffle.gql({
query: {
getPokemon: {
pokemonByName: {
$: {
name: $.String(), // → string
level: $.Int(), // → number
isShiny: $.Boolean(), // → boolean
id: $.ID(), // → string
},
name: true,
},
},
},
})
Available type hints:
$.String()
- Maps to TypeScriptstring
$.Int()
- Maps to TypeScriptnumber
$.Float()
- Maps to TypeScriptnumber
$.Boolean()
- Maps to TypeScriptboolean
$.ID()
- Maps to TypeScriptstring
Type hints can be combined with modifiers:
$.String().required() // Required string variable
$.Int().default(10) // Optional number with default
$.Boolean().as('flag') // Boolean with custom name
Automatic Hoisting
Automatic hoisting extracts all arguments as GraphQL variables without requiring explicit $
markers. This is the default behavior controlled by the hoistArguments
setting.
Default Behavior
By default, hoistArguments: true
automatically extracts all arguments:
const doc = Graffle.gql({
query: {
getTrainer: {
trainerByName: {
$: { name: 'Ash' },
name: true,
class: true,
},
},
},
})
Generates:
query($name: String!) {
trainerByName(name: $name) {
name
class
}
}
# Variables: { "name": "Ash" }
Disabling Automatic Hoisting
You can disable automatic hoisting globally:
import { staticBuilderDefaults } from 'graffle/extensions/document-builder'
staticBuilderDefaults.hoistArguments = false
const doc = Graffle.gql({
query: {
getTrainer: {
trainerByName: {
$: { name: 'Ash' },
name: true,
},
},
},
})
Generates:
query {
trainerByName(name: "Ash") {
name
}
}
Manual Hoisting Always Applies
Explicit $
markers for manual hoisting are always extracted as variables, regardless of the hoistArguments
setting.
Example showing manual hoisting takes precedence:
import { $ } from 'graffle'
import { staticBuilderDefaults } from 'graffle/extensions/document-builder'
staticBuilderDefaults.hoistArguments = false
const doc = Graffle.gql({
query: {
getPokemon: {
pokemonByName: {
$: {
name: $('pokemonName'), // Always extracted
},
name: true,
trainer: {
name: true,
},
},
},
},
})
Generates:
query($pokemonName: String!) {
pokemonByName(name: $pokemonName) {
name
trainer {
name
}
}
}
Conflict Resolution
When both manual hoisting (explicit $
markers) and automatic hoisting want the same variable name, Graffle performs automatic renaming to avoid conflicts:
import { $ } from 'graffle'
const doc = Graffle.gql({
query: {
getTrainer: {
trainerByName: {
$: {
name: $('trainerName'), // Gets: $trainerName
},
name: true,
class: true,
},
},
},
})
Generates:
query($trainerName: String!) {
trainerByName(name: $trainerName) {
name
class
}
}
Choosing an Approach
Use Manual Hoisting when:
- You want explicit control over which arguments become variables
- You're building reusable query fragments
- You need fine-grained control over variable naming and types
- You're working in schema-less mode and need type hints
Use Automatic Hoisting when:
- You want all arguments to be variables (default best practice)
- You're building simple queries where explicit control isn't needed
- You want to minimize boilerplate code
- You trust Graffle's automatic variable naming
Both approaches can be combined in the same document, with manual hoisting always taking precedence over automatic hoisting.