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 unaffected —
optimisticResponse 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).
Version
@apollo/client4.2.3 (modern signatures active viaDeclareDefaultOptions)Summary
Under modern signatures, the inferred
MutateResult.errorshould reflect the resolvederrorPolicy. With the defaulterrorPolicy: 'none'a mutation rejects on error, so the result can never carry anerror— and it's correctly typedundefined.However, simply adding an
optimisticResponseto the sameclient.mutatecall changes the inferrederrortype toErrorLike | undefined, even thougherrorPolicyis unchanged (still'none') and the runtime behavior is identical (the promise still rejects on error —optimisticResponseonly affects the cache write/rollback). So two otherwise-identical mutations get differenterrortypes 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
Expected
withOptimistic.errorshould be typedundefined(same aswithoutOptimistic.error), since the resolvederrorPolicyis'none'andoptimisticResponsehas no effect onerrorPolicyor error handling.Actual
withOptimistic.erroris typedErrorLike | undefined;withoutOptimistic.errorisundefined.Notes
optimisticResponsedoesn't changeerrorPolicy; both calls reject on error. This is a type-only inconsistency.DeclareDefaultOptions.Mutate { errorPolicy: 'none' }(and setting the matching runtimedefaultOptions.mutate) does not fix it — theoptimisticResponsecall still resolves toErrorLike | undefined.optimisticResponse(e.g. a flat{ foo: 'x' }againstTData = { 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 theoptimisticResponse(Unmasked<NoInfer<TData>>) inference and howOptionWithFallback<TOptions, DefaultOptions, "errorPolicy">resolveserrorPolicyfrom the inferredTOptions.Impact
Mutation result error types are inconsistent across call sites depending on an unrelated option, so code can't rely on
errorbeing correctly narrowed (some sites get a usableErrorLike | undefined, others getundefinedand fail onerror.message).