Skip to content

client.mutate result error type widens to ErrorLike | undefined when optimisticResponse is provided (modern signatures) #13285

Description

@ItaiYosephi

Version

@apollo/client 4.2.3 (modern signatures active via DeclareDefaultOptions)

Summary

Under modern signatures, the inferred MutateResult.error should reflect the resolved errorPolicy. With the default errorPolicy: 'none' a mutation rejects on error, so the result can never carry an error — and it's correctly typed undefined.

However, simply adding an optimisticResponse to the same client.mutate call changes the inferred error type to ErrorLike | undefined, even though errorPolicy is unchanged (still 'none') and the runtime behavior is identical (the promise still rejects on error — optimisticResponse only affects the cache write/rollback). So two otherwise-identical mutations get different error types based solely on the presence of an unrelated option.

This is purely a type-inference issue (no runtime difference). It's in the same defaultOptions/signature-inference area as #13136.

Reproduction

import { ApolloClient } from '@apollo/client';
import type { TypedDocumentNode } from '@apollo/client';

// Activate "modern" signatures (any non-optional DeclareDefaultOptions does this).
declare module '@apollo/client' {
  namespace ApolloClient {
    namespace DeclareDefaultOptions {
      interface WatchQuery {
        errorPolicy: 'all';
      }
    }
  }
}

interface Payload {
  updateThing: {
    __typename: 'Payload';
    inner: { __typename: 'Inner'; data: Record<string, unknown> };
  };
}

const MUTATION = {} as TypedDocumentNode<Payload, { input: Record<string, unknown> }>;

declare const client: ApolloClient;

export async function repro() {
  // No errorPolicy anywhere => default 'none' => mutate rejects on error,
  // so `error` can never resolve and should be `undefined` in both cases.

  const withoutOptimistic = await client.mutate({
    mutation: MUTATION,
    variables: { input: {} },
  });
  const _a: undefined = withoutOptimistic.error; // ✅ OK — error is `undefined`

  const withOptimistic = await client.mutate({
    mutation: MUTATION,
    variables: { input: {} },
    optimisticResponse: {
      updateThing: {
        __typename: 'Payload',
        inner: { __typename: 'Inner', data: { x: 1 } },
      },
    },
  });
  const _b: undefined = withOptimistic.error;
  // ❌ TS2322: Type 'ErrorLike | undefined' is not assignable to type 'undefined'.
}

Expected

withOptimistic.error should be typed undefined (same as withoutOptimistic.error), since the resolved errorPolicy is 'none' and optimisticResponse has no effect on errorPolicy or error handling.

Actual

withOptimistic.error is typed ErrorLike | undefined; withoutOptimistic.error is undefined.

Notes

  • Runtime is unaffectedoptimisticResponse doesn't change errorPolicy; both calls reject on error. This is a type-only inconsistency.
  • Declaring DeclareDefaultOptions.Mutate { errorPolicy: 'none' } (and setting the matching runtime defaultOptions.mutate) does not fix it — the optimisticResponse call still resolves to ErrorLike | undefined.
  • A fully-concrete optimisticResponse (e.g. a flat { foo: 'x' } against TData = { foo: string }) does not reproduce it. It appears to require a payload field typed with an index signature / Record<...> (which is common for JSON-ish fields) — pointing at an interaction between the optimisticResponse (Unmasked<NoInfer<TData>>) inference and how OptionWithFallback<TOptions, DefaultOptions, "errorPolicy"> resolves errorPolicy from the inferred TOptions.

Impact

Mutation result error types are inconsistent across call sites depending on an unrelated option, so code can't rely on error being correctly narrowed (some sites get a usable ErrorLike | undefined, others get undefined and fail on error.message).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions