Variables

Variables allow you to parameterize fragments and operations. soda-gql uses a unique type syntax that provides full type safety while remaining concise.

How soda-gql Variables Differ from GraphQL

Standard GraphQL uses this syntax for variables:

query GetUser($id: ID!, $limit: Int) {
  user(id: $id) {
    posts(limit: $limit) { ... }
  }
}

soda-gql uses a TypeScript-based approach:

AspectGraphQLsoda-gql
Syntax$name: Type!$var("name").scalar("Type:!")
RequiredType! (suffix)"Type:!" (suffix after colon)
OptionalType (no suffix)"Type:?" (explicit optional)
Lists[Type!]!"Type:![]!"
DeclarationIn operation headerArray: variables: [$var(...)]
Type SafetyExternal codegen requiredCompile-time inference
Explicit Nullability

Unlike GraphQL where omitting ! means optional, soda-gql requires explicit :! or :? to make nullability clear and prevent mistakes.

Declaring Variables

Variables are declared using $var() in the variables array:

gql.default(({ query }, { $var }) =>
  query.operation(
    {
      name: "SearchPosts",
      variables: [
        //
        $var("query").scalar("String:!"),      // Required string
        $var("limit").scalar("Int:?"),         // Optional int
        $var("tags").scalar("String:![]?"),    // Optional list of required strings
      ],
    },
    ({ f, $ }) => [...],
  ),
);

Type Specifier Syntax

The type specifier follows this pattern: "TypeName:nullability[listNullability]..."

Basic Types

SpecifierMeaningGraphQL Equivalent
"ID:!"Required IDID!
"ID:?"Optional IDID
"String:!"Required StringString!
"String:?"Optional StringString
"Int:!"Required IntInt!
"Float:?"Optional FloatFloat
"Boolean:!"Required BooleanBoolean!

List Types

Lists add [] with their own nullability:

SpecifierMeaningGraphQL Equivalent
"String:![]!"Required list of required strings[String!]!
"String:![]?"Optional list of required strings[String!]
"String:?[]!"Required list of optional strings[String]!
"String:?[]?"Optional list of optional strings[String]

Nested Lists

For lists of lists, chain the brackets:

SpecifierGraphQL Equivalent
"Int:![]![]!"[[Int!]!]!
"String:?[]?[]?"[[String]]

Custom Types

Use your schema's input types and custom scalars:

$var("input").scalar("CreateUserInput:!")    // Custom input type
$var("cursor").scalar("Cursor:?")            // Custom scalar
$var("filters").scalar("FilterInput:![]?")   // List of custom input

Using Variables

In Field Arguments

Reference declared variables using $:

({ f, $ }) => [
  //
  f.user({ id: $.userId })(({ f }) => [...]),
  f.posts({ limit: $.limit, tags: $.tags })(({ f }) => [...]),
]

Passing to Embedded Fragments

Pass variables to embedded fragments:

// Fragment with its own variable
const userFragment = gql.default(({ fragment }, { $var }) =>
  fragment.Query(
    { variables: [$var("userId").scalar("ID:!")] },
    ({ f, $ }) => [
      //
      f.user({ id: $.userId })(({ f }) => [
        //
        f.id(),
        f.name(),
        f.email(),
      ]),
    ],
  ),
);

// Operation passing its variable to the fragment
const getUserQuery = gql.default(({ query }, { $var }) =>
  query.operation(
    {
      name: "GetUser",
      variables: [$var("userId").scalar("ID:!")],
    },
    ({ $ }) => [
      //
      // Pass operation variable to fragment variable
      userFragment.embed({ userId: $.userId }),
    ],
  ),
);

Built-in Scalar Types

soda-gql recognizes these standard GraphQL scalars:

TypeTypeScript TypeDescription
IDstringUnique identifier
StringstringUTF-8 string
Intnumber32-bit signed integer
FloatnumberDouble-precision float
Booleanbooleantrue/false

Custom scalars are defined in your project's inject file:

// In your default.inject.ts
import { defineScalar } from "@soda-gql/core/adapter";

export const scalar = {
  ...defineScalar("DateTime", ({ type }) => ({
    input: type<string>(),   // ISO string when sending
    output: type<Date>(),    // Date object when receiving
    directives: {},
  })),
} as const;

Type Inference

Variable types are fully inferred:

const query = gql.default(({ query }, { $var }) =>
  query.operation(
    {
      name: "Search",
      variables: [
        //
        $var("query").scalar("String:!"),
        $var("limit").scalar("Int:?"),
      ],
    },
    ({ f, $ }) => [...],
  ),
);

// Inferred type
type Variables = typeof query.$infer.input;
// { query: string; limit?: number }

When calling the query, TypeScript enforces correct variable types:

// Correct
graphqlClient({
  document: query.document,
  variables: { query: "hello", limit: 10 },
});

// Error: 'query' is required
graphqlClient({
  document: query.document,
  variables: { limit: 10 },
});

// Error: 'limit' must be number or undefined
graphqlClient({
  document: query.document,
  variables: { query: "hello", limit: "10" },
});

Next Steps