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