Skip to content
Graffle is a work in progress. Learn more.

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:

graphql
query {
  pokemonByName(name: "Pikachu") {
    name
    hp
  }
}

With hoisting, arguments become variables:

graphql
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

ts
import { $ } from 'graffle'

const doc = Graffle.gql({
  query: {
    getPokemon: {
      pokemonByName: {
        $: { name: $('pokemonName').required() },
        name: true,
        type: true,
      },
    },
  },
})

Generates:

graphql
query getPokemon($pokemonName: String!) {
  pokemonByName(name: $pokemonName) {
    name
    type
  }
}

Variable Modifiers

The object syntax provides additional control over manually hoisted variables:

ts
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:

ts
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 TypeScript string
  • $.Int() - Maps to TypeScript number
  • $.Float() - Maps to TypeScript number
  • $.Boolean() - Maps to TypeScript boolean
  • $.ID() - Maps to TypeScript string

Type hints can be combined with modifiers:

ts
$.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:

ts
const doc = Graffle.gql({
  query: {
    getTrainer: {
      trainerByName: {
        $: { name: 'Ash' },
        name: true,
        class: true,
      },
    },
  },
})

Generates:

graphql
query($name: String!) {
  trainerByName(name: $name) {
    name
    class
  }
}

# Variables: { "name": "Ash" }

Disabling Automatic Hoisting

You can disable automatic hoisting globally:

ts
import { staticBuilderDefaults } from 'graffle/extensions/document-builder'

staticBuilderDefaults.hoistArguments = false

const doc = Graffle.gql({
  query: {
    getTrainer: {
      trainerByName: {
        $: { name: 'Ash' },
        name: true,
      },
    },
  },
})

Generates:

graphql
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:

ts
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:

graphql
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:

ts
import { $ } from 'graffle'

const doc = Graffle.gql({
  query: {
    getTrainer: {
      trainerByName: {
        $: {
          name: $('trainerName'), // Gets: $trainerName
        },
        name: true,
        class: true,
      },
    },
  },
})

Generates:

graphql
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.

Released under the MIT License.