Mesrai
Back to blog
// essayTechnical Deep Dive

exactOptionalPropertyTypes Bugs Mesrai Catches Across Boundaries

Real PR catch: patch builder mixing `{ name: undefined }` and `{}` semantics — exactOptionalPropertyTypes draws them apart. Build-by-keys fix.

Mesrai TeamAugust 15, 20268 min read

A patch-builder for a user update. Mixes `{ name: undefined }` and `{}` as if they were the same. With `exactOptionalPropertyTypes` enabled, they are not — explicit undefined is a different state from absent.

The vulnerable diff

tslib/patch-user.ts
// lib/patch-user.ts
interface UserPatch { name?: string; }

function buildPatch(input: { name?: string }): UserPatch {
  // BUG under exactOptionalPropertyTypes:
  // { name: undefined } is NOT assignable to UserPatch where
  // name is optional but not explicitly `| undefined`.
  return { name: input.name };
}

What is wrong

TypeScript 4.4+ ships `exactOptionalPropertyTypes`. With it on, an optional property `name?: string` accepts an absent key but rejects an explicit `undefined`. The intuition: 'optional' (key may be absent) and 'nullable' (key present with value undefined) become distinct. Code that built patches with `{ name: undefined }` to mean 'leave unchanged' now needs to either omit the key entirely or change the type to `name?: string | undefined`.

The attack

Compile error:

textexploit
error TS2375: Type '{ name: string | undefined }' is not assignable to
type 'UserPatch' with 'exactOptionalPropertyTypes: true'. Consider
adding 'undefined' to the types of the target's properties.

Without the strict flag, the code compiles but consumers (e.g. ORMs) may interpret 'undefined' differently from 'absent' — sometimes clearing the field instead of preserving.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Language] [Type-Subtlety] [low]

Under exactOptionalPropertyTypes, `{ name: undefined }` and `{}`
are distinct. The patch builder returns the former for an absent
input; either change the type to allow undefined or omit the key:

  // Option A: distinguish at build time, omit absent keys
  function buildPatch(input: { name?: string }): UserPatch {
    const patch: UserPatch = {};
    if (input.name !== undefined) patch.name = input.name;
    return patch;
  }

  // Option B: widen the type to accept either
  interface UserPatch { name?: string | undefined; }

ORM behavior matters here. Prisma treats `{ name: undefined }` as
"leave unchanged" but `{}` is also "leave unchanged" — same effect.
Drizzle and others may differ.

The fix

tslib/patch-user.ts (fixed)
// lib/patch-user.ts — fixed (option A)
function buildPatch(input: { name?: string }): UserPatch {
  const patch: UserPatch = {};
  if (input.name !== undefined) patch.name = input.name;
  return patch;
}

Build the patch by adding only present keys. The output now contains exactly what changed; downstream code can rely on the distinction.

Why human review missed it

exactOptionalPropertyTypes is a 2022+ TypeScript option many codebases haven't adopted yet. The bug shows up only when teams turn it on in tsconfig. Mesrai catches mixed-semantics patterns whether the flag is on or off.

Related rules + further reading

Mesrai rule pack: language/exact-optional-property-types — flags `{ field: undefined }` produced for fields typed as `field?:T`.

TypeScript: exactOptionalPropertyTypes compiler option.

Common when migrating older codebases to stricter type configs.

Takeaway

Optional ≠ explicit-undefined under strict TypeScript. Build patches by adding only present keys. Mesrai catches the difference.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.