nitrogql logonitrogql

Configuring Scalar Types

nitrogql supports configuring the mapping between GraphQL scalar types and TypeScript types. This is useful when you want to use custom scalar types in your GraphQL schema. The mapping is configured with the generate.type.scalarTypes option.

This page explains how to configure scalar types for different scenarios.

πŸ˜Άβ€πŸŒ«οΈ This page does not cover the nitrogql:graphql-scalars-plugin. If you want to use custom scalar from GraphQL Scalars, you should read that page instead.

Four Different Situations

nitogql supports specifying different TypeScript types for one GraphQL scalar type. They are used in different situations:

NameLocationSituationExample (ID)
resolverInputserveras an argument of a resolverstring
resolverOutputserveras a return value of a resolverstring | number
operationInputclientas an argument passed to an operationstring | number
operationOutputclientas a return value of an operationstring

The first two types are used in the server, and the last two types are used in the client.

An example of a scalar type that uses different types in different situations is the ID scalar type. According to the GraphQL spec, an ID value is always serialized as a string. However, it can be represented as a string or an integer in the server. Above table shows how the ID scalar type is configured for different situations by default.

resolverInput

The resolverInput type is used in a GraphQL server when executing a resolver. Whenever a resolver receives an argument of the scalar type, it will be of this type.

Example:

type Query {
  getUser(id: ID!): User!
}
const getUser: Resolvers<Context>["Query"]["getUser"] = async (
  _,
  { id },
) => {
  // `id` is typed as its `resolverInput` variant (string)
  // ...
};

resolverOutput

The resolverOutput type is used in a GraphQL server when executing a resolver. Whenever a resolver returns a value of the scalar type, it will be of this type.

Example:

type Query {
  me: User!
}
const me: Resolvers<Context>["Query"]["me"] = async () => {
  return {
    // `id` is typed as its `resolverOutput` variant (string | number)
    id: 1,
    // ...
  };
};

operationInput

The operationInput type is used in a GraphQL client when executing an operation. Whenever an operation requires an argument of the scalar type, you need to pass a value of this type.

Example:

query GetUser($id: ID!) {
  getUser(id: $id) {
    # ...
  }
}
const result = await yourClient.query({
  query: GetUserQuery,
  variables: {
    // `id` is typed as its `operationInput` variant (string | number)
    id: 1,
  },
});

operationOutput

The operationOutput type is used in a GraphQL client when executing an operation. Whenever an operation returns a value of the scalar type, it will be of this type.

Example:

query Me {
  me {
    id
    # ...
  }
}
const result = await yourClient.query({
  query: MeQuery,
});

// `id` is typed as its `operationOutput` variant (string)
const id = result.data.me.id;

Specifying scalarTypes in Configuration

When you configure the TypeScript types for a scalar type, you can use one of the following three forms. In addition to one that specifies all four types, there are two convenient shorthand forms. Actually, you rarely need to specify all four types, so the shorthand forms are usually enough.

Single Form

The easiest way to configure a scalar type is to use the single form. This form specifies to use one TypeScript type for all four situations.

Example:

scalarTypes:
  String: string

With this setting, the String scalar type will be mapped to the string type in all four situations.

Fundamental scalar types such as String, Int, Float and Boolean are configured with the single form by default. These types always map to the same TypeScript types (i.e. string, number, number and boolean, respectively).

Send/Receive Form

The send/receive form is another shorthand form that specifies two TypeScript types for a scalar type: one for send situations, and the other for receive situations. They map to the four situations as follows:

NameSend/Receive
resolverInputreceive
resolverOutputsend
operationInputsend
operationOutputreceive

Among the built-in scalar types, the ID scalar type is configured with the send/receive form by default.

scalarTypes:
  ID:
    send: string | number
    receive: string

With this setting, the ID scalar type will be mapped to the string | number type in the send situations, and to the string type in the receive situations.

This setting reflects the fact that an ID value is always serialized as a string, but the server can also accept an integer and coerce it to a string.

The coercion takes place when you receive a value from the network; namely, when you receive an operation output and when you receive a resolver input. Hence the receive type used for these situations.

In contrast, when you send a value to the network, you can specify a value before coercion. This applies to when you generate a resolver output (that will be sent through the network) and when you specify an operation input.

Separate Form

The separate form allows you to specify four different types separately. This form is not used by built-in scalars.

You can use this form if you want to have full control on what type is used in each situation.

This form is supported for completeness, but we are not aware of any use case. If you know one, please get in touch!

Example:

scalarTypes:
  Date:
    resolverInput: string
    resolverOutput: Date | string
    operationInput: string
    operationOutput: string

Notes on GraphQL Code Generator Compatibility

If you know or trying to migrate from GraphQL Code Generator, you may wonder about compatibility with GraphQL Code Generator's scalars option.

Particularly, GraphQL Code Generator supports specifying a scalar's type as a pair of input and output types.

You should note that the input/output configuration is not equivalent to nitrogql's send/receive configuration mode. The following table shows the correspondence between the two:

Nameinput/outputsend/receive
resolverInputinputreceive
resolverOutputoutputsend
operationInputinputsend
operationOutputoutputreceive

We have chosen to diverge from GraphQL Code Generator here because we found that the send/receive model better reflects the reality than the input/output model.

When you are migrating from GraphQL Code Generator, we recommend to translate the input/output setting to the send/receive setting.

If you need to keep the behavior as-is, you can use the separate form instead.