Mesrai
Back to blog
// essayTechnical Deep Dive

Object Spread Replacing Required Fields: The Type-Holes Mesrai Closes

Real PR catch: spread merge where Partial<T> override has `undefined` — required field becomes undefined. Filter fix.

Mesrai TeamAugust 13, 20268 min read

A config merge helper. Spreads defaults, then overrides. Author thought required defaults would survive. They do not — an explicit `undefined` in overrides replaces the default, even though the type stays 'required'.

The vulnerable diff

tslib/merge-config.ts
// lib/merge-config.ts
interface Config { host: string; port: number; }

function merge(defaults: Config, overrides: Partial<Config>): Config {
  // BUG: if overrides.host is explicitly undefined, host: undefined.
  return { ...defaults, ...overrides };
}

const r = merge(
  { host: "api.example", port: 443 },
  { host: undefined, port: 8080 }   // explicit undefined survives
);
// r.host is undefined at runtime; r.host: string per the type.

What is wrong

Object spread copies all enumerable properties from the source, including `undefined` values. The result has `host: undefined` even though the type still claims `host: string`. This is one of TypeScript's known soundness gaps with `Partial<T>` and spread. The fix is to filter undefined values from the overrides before spreading, or to use a merge helper that respects required-vs-optional semantics.

The attack

Symptom: downstream code reading r.host crashes with `Cannot read property of undefined`.

textexploit
const r = merge(defaults, { host: undefined });
new URL(r.host); // TypeError: Invalid URL (undefined)

TypeScript happily allows it because Partial<Config> permits undefined values.

Mesrai's review comment

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

[mesrai] [code-review] [Language] [Type-Hole] [medium]

Spread propagates explicit `undefined` from overrides, overriding
required defaults. Type system claims host: string, runtime has
host: undefined.

Filter undefined before spreading:

  function merge(defaults: Config, overrides: Partial<Config>): Config {
    const cleanOverrides = Object.fromEntries(
      Object.entries(overrides).filter(([_, v]) => v !== undefined)
    );
    return { ...defaults, ...cleanOverrides };
  }

Or use a library that handles this correctly (lodash.merge, deepmerge
both have variants that skip undefined).

For TS strict-mode: enable `exactOptionalPropertyTypes: true` in
tsconfig. Then `host: undefined` is a type error unless the field is
typed as `host?: string | undefined` explicitly.

The fix

tslib/merge-config.ts (fixed)
// lib/merge-config.ts — fixed
function merge(defaults: Config, overrides: Partial<Config>): Config {
  const cleanOverrides = Object.fromEntries(
    Object.entries(overrides).filter(([_, v]) => v !== undefined)
  );
  return { ...defaults, ...cleanOverrides } as Config;
}

Filter undefined values before spreading. Defaults survive. Type matches runtime.

Why human review missed it

Spread soundness with `Partial<T>` is a known TypeScript trap that affects many config-merge helpers. Mesrai's rule pack catches spread merges where the source is `Partial<T>` and the target requires all-defined.

Related rules + further reading

Mesrai rule pack: language/no-undefined-override — flags spread that can introduce undefined into required fields.

TypeScript: exactOptionalPropertyTypes compiler option.

Common in CLI / config / settings code.

Takeaway

Filter undefined before spreading required defaults. Or use exactOptionalPropertyTypes. Mesrai catches every unsafe spread.

// try

See it on your next PR.

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