Skip to content

Commit

Permalink
Allow an arg to be optional and not nullable if a default is provided
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Mar 2, 2024
1 parent a5bc9f2 commit ab43016
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 10 deletions.
21 changes: 12 additions & 9 deletions src/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,11 +1139,22 @@ class Extractor {
return this.report(node.name, E.expectedNullableArgumentToBeOptional());
}

if (node.questionToken) {
let defaultValue: ConstValueNode | null = null;
if (defaults != null) {
const def = defaults.get(node.name.text);
if (def != null) {
defaultValue = this.collectConstValue(def);
}
}

if (node.questionToken && defaultValue == null) {
// Question mark means we can handle the argument being undefined in the
// object literal, but if we are going to type the GraphQL arg as
// optional, the code must also be able to handle an explicit null.
//
// ... unless there is a default value. In that case, the default will be
// used argument is omitted or references an undefined variable.

// TODO: This will catch { a?: string } but not { a?: string | undefined }.
if (type.kind === Kind.NON_NULL_TYPE) {
return this.report(node.questionToken, E.nonNullTypeCannotBeOptional());
Expand All @@ -1153,14 +1164,6 @@ class Extractor {

const description = this.collectDescription(node);

let defaultValue: ConstValueNode | null = null;
if (defaults != null) {
const def = defaults.get(node.name.text);
if (def != null) {
defaultValue = this.collectConstValue(def);
}
}

const deprecatedDirective = this.collectDeprecated(node);

return this.gql.inputValueDefinition(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @gqlType */
export default class SomeType {
/** @gqlField */
hello({ greeting = "Hello" }: { greeting?: string }): string {
return `${greeting ?? "Hello"} World!`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-----------------
INPUT
-----------------
/** @gqlType */
export default class SomeType {
/** @gqlField */
hello({ greeting = "Hello" }: { greeting?: string }): string {
return `${greeting ?? "Hello"} World!`;
}
}

-----------------
OUTPUT
-----------------
-- SDL --
type SomeType {
hello(greeting: String! = "Hello"): String @metadata(argCount: 1)
}
-- TypeScript --
import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql";
export function getSchema(): GraphQLSchema {
const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({
name: "SomeType",
fields() {
return {
hello: {
name: "hello",
type: GraphQLString,
args: {
greeting: {
name: "greeting",
type: new GraphQLNonNull(GraphQLString),
defaultValue: "Hello"
}
}
}
};
}
});
return new GraphQLSchema({
types: [SomeTypeType]
});
}
31 changes: 30 additions & 1 deletion website/docs/04-docblock-tags/03-arguments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function userById(_: Query, args: { id: string }): User {

## Default values

Default values for arguments can be defined by using the `=` operator with destructuring. Note tha tyou must perform the destructuring in the argument list, not in the function body:
Default values for arguments can be defined by using the `=` operator with destructuring. Note that you must perform the destructuring in the argument list, not in the function body:

```ts
class MyClass {
Expand Down Expand Up @@ -55,6 +55,33 @@ type GreetingConfig = {
};
```

## Nullable arguments

If you define your argument as nullable in your GraphQL schema, `graphql-js` may pass either an explicit `null` if the user passes an explicit null or simply not define the argument if the user omits the argument or passes it a nullable variable which ends up not being passed to the operation.

To account for this, Grats will require that any argument that is either nullable (`someArg: T | null`) or optional (`someArg?: T`) be defined as _both_ nullable and optional: `someArg?: T | null`.

This ensures your resolver code handles both possible casses.

The one exception is if your argument has a default value. In that case, you may opt to mark your argument as optional but not nullabe.

```ts
/** @gqlField */
export function greeting(_: Query, { name: "Max" }: { name?: string }): string {
return `Hello, ${name}`;
}
```

Will result in the following schema:

```graphql
type Query {
greeting(name: String! = "Max"): String!
}
```

Note that the `name` argument is marked as non-nullable in the schema. This means the user may not pass an explicit `null`, but if the argument is omitted, the default value will be used.

## Deprecated arguments

Optional arguments can be marked as `@deprecated` by using the `@deprecated` JSDoc tag:
Expand All @@ -70,3 +97,5 @@ class MyClass {
}
}
```

import ts from "typescript";

0 comments on commit ab43016

Please sign in to comment.