InteractiveFrameworks

Enums and scalars

Give values their real types: a registered enum the schema lists and checks, dates that travel as ISO strings and arrive as Date objects, and a custom Microchip scalar that validates what clients send and formats what they get.

What you'll learn

  • Register a TypeScript enum with registerEnumType(), with descriptions, and use it for fields and arguments
  • Use Date fields and arguments, which GraphQL carries as the DateTime scalar in ISO 8601
  • Write a custom scalar with GraphQLScalarType: serialize for answers, parseValue for variables, parseLiteral for inline values
  • Tell where each kind of value is checked: enums and scalars by GraphQL before any resolver runs

A cat's breed is not any string: the shelter knows three breeds, and "Persain" is a typo, not a fourth. Its arrival is a moment in time, not text. A microchip number has a format that staff type in three different ways. So far all of these would be String, and every resolver would check and convert them by hand. GraphQL's type system can carry the meaning instead: an enum lists the allowed values, and a scalar defines how a value is written on the wire and what it becomes in your code. The schema publishes both, and GraphQL checks both before any resolver runs.

Enums

A TypeScript enum becomes a GraphQL enum once it is registered:

export enum Shift { MORNING = 'MORNING', EVENING = 'EVENING' }

registerEnumType(Shift, {
  name: 'Shift',
  description: 'When a volunteer works',
  valuesMap: { EVENING: { description: 'Until closing time' } },
});

A field or argument then names it: @Field(() => Shift). Leave the type function out and Nest sees only the runtime type of a string enum's values, a String, so the schema quietly loses the enum. Clients write enum values without quotes (shifts(when: EVENING)) and receive them as strings. A value that is not in the enum is refused with a 400, Value "NIGHT" does not exist in "Shift" enum., and your resolver receives the TypeScript enum's value.

Dates

GraphQL has no date type, so Nest provides scalars for one. A property typed Date becomes the DateTime scalar (GraphQLISODateTime): it travels as an ISO 8601 string, 2026-06-01T09:00:00.000Z, and resolvers see a real Date. An argument or an args-class field needs the type function, () => GraphQLISODateTime, because an optional Date property carries no type at runtime. buildSchemaOptions: { dateScalarMode: 'timestamp' } switches to milliseconds since the epoch, if clients prefer that.

Custom scalars

A scalar is a GraphQLScalarType from the graphql package with three functions, one for each way a value crosses the boundary:

export const PostcodeScalar = new GraphQLScalarType({
  name: 'Postcode',
  serialize: (value) => format(value as string),
  parseValue: (value) => validate(value),
  parseLiteral: (ast) => validate(ast.kind === Kind.STRING ? ast.value : undefined),
});
  • serialize turns your value into what goes out in an answer.
  • parseValue turns a value that came as a variable into your value.
  • parseLiteral does the same for a value written inline in the query. It receives the value as a syntax node, so it checks the node's kind first.

To refuse a value, throw. A GraphQLError is reported as it is: the client gets its message and a 400 before any resolver runs. Throw a plain Error and GraphQL wraps it in its own sentence (Expected value of type "Postcode!", found "X"; ...). A field or argument names the scalar through the type function, @Field(() => PostcodeScalar). The docs also register it under resolvers in forRoot(); code first finds the scalar through the fields that name it, so this lesson does not need that.

The point of a scalar is that the rest of the code never sees a badly formed value. Resolvers receive digits, not "whatever the client typed".

Your task

Cats now have a breed enum, an arrival date and an optional microchip. The service filters by breed and arrival, and records microchips as 15 bare digits.

  1. Register Breed under the name Breed, described as What the cat looks like, with MAINE_COON described as The big ones.
  2. Write the Microchip scalar. A client sends 15 digits, grouped by spaces or dashes or not at all. Anything else is refused with a GraphQLError whose message is A microchip is 15 digits. Inside, the value is the 15 digits alone. In an answer it is written in groups of three: 985-112-345-678-901.
  3. Type the Cat fields: breed a Breed, arrivedAt a date and time, microchip a Microchip that may be null.
  4. Type CatsArgs: an optional breed and an optional arrivedAfter date.
  5. Add a registerMicrochip(catId: Int!, chip: Microchip!) mutation returning the cat.

When it fails

  • Cannot determine a GraphQL input type for the "breed" at startup: the enum was never registered.
  • breed works but the schema says String: its field has no () => Breed.
  • Every microchip is refused, even valid ones: parseLiteral read ast.value without checking that the node is a string, or parseValue expected grouping the client did not send.
  • Answers show 985112345678902: serialize does not format the digits.

Remember

  • registerEnumType() makes a TypeScript enum a GraphQL enum; fields and arguments name it with a type function.
  • Date travels as the ISO DateTime scalar; arguments need () => GraphQLISODateTime.
  • A scalar is serialize out, parseValue for variables, parseLiteral for inline values.
  • Throw a GraphQLError to refuse a value; it never reaches a resolver.
Stuck? Show a hint

breed.enum.ts: registerEnumType(Breed, { name: 'Breed', description, valuesMap: { MAINE_COON: { description } } }). cat.model.ts: @Field(() => Breed), a bare @Field() on a Date, @Field(() => MicrochipScalar, { nullable: true }). cats.args.ts: @Field(() => Breed, { nullable: true }) and @Field(() => GraphQLISODateTime, { nullable: true }). microchip.scalar.ts: serialize groups the digits, parseValue and parseLiteral (a Kind.STRING literal) strip spaces and dashes and throw a GraphQLError unless 15 digits remain. The mutation: @Args('chip', { type: () => MicrochipScalar }).