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.
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.
- TypeScript
- GraphQL
/**
* @gqlInput
* @oneOf
*/
export type UserBy = { email: string } | { username: string };
input UserBy @oneOf {
email: String
username: String
}
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:
- TypeScript
- GraphQL
/**
* @gqlInput
* @oneOf
*/
export type UserBy =
| {
/** Fetch the user by email */
email: string;
}
| {
/** Fetch the user by username */
username: string;
};
input UserBy @oneOf {
"""Fetch the user by email"""
email: String
"""Fetch the user by username"""
username: String
}
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:
- TypeScript
- GraphQL
/**
* @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);
}
}
input UserBy @oneOf {
email: String
username: String
}
type Query {
getUser(by: UserBy!): User
}
type User {
email: String
username: String
}
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.