Skip to main content

OneOf Input

OneOf input types are an experimental GraphQL feature, currently a draft stage RFC, which lets you define an input type where exactly one of the fields must be provided. This can be useful for modeling structures similar to unions but for input types.

OneOf inputs can be defined by placing both @gqlInput and @oneOf in a docblock directly before a:

  • Type alias of a union of object literal types

The union must be a union of object types, and each object type must have exactly one property whose value is a valid input type. You can think of each member of the union as modeling one of the possible values the user might provide as a valid input.

info

OneOf is not supported in versions of graphql-js earlier than v16.9.0. If you are using an older version of graphql-js, you will need to upgrade to use this feature.

/**
* @gqlInput
* @oneOf
*/
export type UserBy = { email: string } | { username: string };
Playground

OneOf input field descriptions

TypeScript does not support docblocks "attach" to a member of a union. Therefore, if you want to provide a description for a field of a OneOf input, place it above the field, within the object literal:

/**
* @gqlInput
* @oneOf
*/
export type UserBy =
| {
/** Fetch the user by email */
email: string;
}
| {
/** Fetch the user by username */
username: string;
};
Playground

Working with OneOf inputs

When handling one of these types in TypeScript it is a best practice to use an exhaustive switch. This way, if you add a new option to the union, TypeScript will trigger an error in all the locations where you handle the union.

As of TypeScript 5.3.0, TypeScript supports a pattern called Switch True Narrowing which can be used to ensure that you have handled all the possible options in this type of union. It looks like this:

/**
* @gqlInput
* @oneOf
*/
export type UserBy = { email: string } | { username: string };

/** @gqlField */
export function getUser(_: Query, by: UserBy): User {
switch (true) {
case "email" in by:
return User.fromEmail(by.email);
case "username" in by:
return User.fromUsername(by.username);
default: {
// This line will error if an unhandled option is added to the union
const _exhaustive: never = by;
throw new Error(`Unhandled case: ${JSON.stringify(by)}`);
}
}
}

/** @gqlType */
type Query = unknown;

/** @gqlType */
class User {
constructor(
/** @gqlField */
public email?: string,
/** @gqlField */
public username?: string,
) {}

static fromEmail(email: string): User {
return new User(email, undefined);
}

static fromUsername(username: string): User {
return new User(undefined, username);
}
}
Playground
note

Grats is open to supporting other syntaxes for defining OneOf inputs. If you have a syntax that you find more intuitive, please open an issue.