apollo-client: converting `null` to `undefined`

Intended outcome:

In Apollo 2, I’m trying to convert all null values to undefined in every query response.

Actual outcome:

I get a console warning saying the field is missing. It looks like:

writeToStore.js:114 Missing field locationType in {
  "__typename": "Offense",
  "id": "77205221-5f74-45b9-5268-67f96b55beb4",
  "kind": "INTIMIDATION"

I also get no response back and no errors

How to reproduce the issue:

This is how I define my link:

const httpLink = createHttpLink({
  uri: window.xnGlobals.ENDPOINT_GQL,
  fetch,
})
const nullToUndefined = value => {
  if (isPlainObject(value)) {
    return mapValues(value, nullToUndefined)
  }
  if (isArray(value)) {
    return value.map(nullToUndefined)
  }
  if (value === null) {
    return undefined // THIS SHOULD BE UNDEFINED
  }
  return value
}
const nullLink = new ApolloLink((operation, forward) => forward(operation).map(nullToUndefined))
const link = nullLink.concat(httpLink)

Version

  • apollo-client@2.0.0

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 10
  • Comments: 18 (6 by maintainers)

Most upvoted comments

Is there a thread somewhere on Apollo in which you discuss whether you’d be willing to support adding an option to the apollo server/client, that would replace all nulls with undefineds? For TS devs, the difference between null and undefined is meaningless, and that’s why most of TS devs have dropped the null type completely.

Without that, currently we’re forced to either use nulls (just because GQL client/server uses them), or use helpers like this one:

type RecursivelyReplaceNullWithUndefined<T> = T extends null
  ? undefined // Note: Add interfaces here of all GraphQL scalars that will be transformed into an object
  : T extends Date
  ? T
  : {
      [K in keyof T]: T[K] extends (infer U)[]
        ? RecursivelyReplaceNullWithUndefined<U>[]
        : RecursivelyReplaceNullWithUndefined<T[K]>;
    };

/**
 * Recursively replaces all nulls with undefineds.
 * Skips object classes (that have a `.__proto__.constructor`).
 *
 * Unfortunately, until https://github.com/apollographql/apollo-client/issues/2412
 * gets solved at some point,
 * this is the only workaround to prevent `null`s going into the codebase,
 * if it's connected to a Apollo server/client.
 */
export function replaceNullsWithUndefineds<T>(
  obj: T
): RecursivelyReplaceNullWithUndefined<T> {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const newObj: any = {};
  Object.keys(obj).forEach((k) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const v: any = (obj as any)[k];
    newObj[k as keyof T] =
      v === null
        ? undefined
        : // eslint-disable-next-line no-proto
        v && typeof v === "object" && v.__proto__.constructor === Object
        ? replaceNullsWithUndefineds(v)
        : v;
  });
  return newObj;
}

I’m trying to simplify how I handle default values. undefined is used for ES6 default parameters, React default props and applying defaults in other libraries like lodash. So it’s a more convenient way to indicate a value has not been set for me. Otherwise I need to manually check and apply default values.

I assumed link allowed me to modify a response after it was processed by apollo-client (so that apollo client would use null/undefined internally the same way). Maybe I’m using the wrong method? Is there another hook to modify the response without affecting the library?

If that’s not available, I’m not familiar with the implementation details, but would it be possible to detect if a field is present with hasOwnProperty or 'key' in object instead?

If the issue is with the <Query> component we overwrite it as well with:

export function Query<R: {}>({ children, ...props }: Props<R>) {
  return (
    <ReactApolloQuery fetchPolicy="cache-and-network" {...props}>
      {(response: any = {}) => children(((nullToUndefined(response): any): R))}
    </ReactApolloQuery>
  )
}

But all these are just hacky workarounds. It would be nice to be able to modify the responses of all queries and mutations directly from Apollo middleware

Is there a generally accepted answer to this? It’s quite annoying to have to deal with this on the client side when using Typescript. Reading through the thread it seems like this should still be open.

期待这个问题能被正确的解决

My current approach is to do the following:

const nullToUndefined = value => {
  if (_.isPlainObject(value)) {
    return _.mapValues(value, nullToUndefined)
  }
  if (_.isArray(value)) {
    return value.map(nullToUndefined)
  }
  if (value === null) {
    return undefined
  }
  return value
}
const internal = new ApolloClient({ /* ... */ })

export const client = {
  ...internal,
  mutate: (...args) => internal.mutate(...args).then(nullToUndefined),
  query: (...args) => internal.query(...args).then(nullToUndefined),
}

It seems to do the trick but it’s far from a clean solution