A Small Helper Function for Conditional Requests in Apollo GraphQL

In a project I'm working on, we have a case where many of the queries ran using the autogenerated Apollo GrapQL hooks that depend on the userID. However, this ID might be null at times, such as when the user's authentication session has ended. This leads to issues in two places:

  • The hook will try to run with a query that has no userID, leading the backend to reject it and return no data/error message on null format userID
  • Typescript will complain about userID possibly being undefined or null

The first one is easy to fix using Apollo GraphQL hooks skip option. However as that does not validate the type, this leads to TypeScript complaining that you're trying to pass a null value to a field that does not accept one (as was in our case). To get around this our team had decided to use non-null assertation with the exclamation mark:

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { data } = useRandomQuery({
  variables: {
    userID: userID!,
  },
  skip: !userID,
})

Now this works all the way until someone assets a variable as non-null but forgots to add it to the skip section, leading to runtime errors or unexpected behavior.. Also, since our project is relatively mature and enforces several linting rules to capture these kinds of things, people had started adding a lot eslint-disable-next-line @typescript-eslint/no-non-null-assertion comments into these hooks. I think that you should try to always figure out a way around disabling a rule if you're disabling it more than once in your codebase.

To fix this I created the following function:

type NoUndefinedField<T> = {
  [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>
}
/**
 *
 * @param args query variables for Apollo graphql query
 * @returns either skip: true or variables: NoUndefinedField<TVariables>
 */
export function variablesOrSkip<TVariables>(
  args: Partial<TVariables>,
): { skip: true } | { variables: NoUndefinedField<TVariables>; skip: false } {
  const hasInvalidValues = Object.values(args).some(
    (value) => value === null || value === undefined,
  )

  if (hasInvalidValues) {
    return { skip: true }
  } else {
    return { variables: args as NoUndefinedField<TVariables>, skip: false }
  }
}

What this function does, is that it checks if any of the passed variables are null or undefined by iterating through them, and if any of them are, it will return the skip parameter as true. This is how to use it:

const { data } = useRandomQuery({
  ...variablesOrSkip({ userID }),
})

Now if it turns out that userID is undefined or null for some reason the query is not run and you don't end up with nasty or embarrassing problems in productions. Most of the code probably makes a lot of sense but I want to highlight this part:

type NoUndefinedField<T> = {
  [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>
}

This code here is not my invention, but instead something I found while searching for a way to remove undefined and null values from the returned type which led me to come across this Stack Overflow discussion about removing null or undefined values of a type