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
// 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:
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
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
// 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.