Interesting question. According to this issue, the result of the spread operator is intended to not trigger excess property checks.
// barObject has more props than Foo, but spread does not trigger excess property checks
const fooObject: Foo = { ...barObject };
// b is explicit property, so checks kicks in here
const foo: Foo = { ...bar, b: 2 }
There aren't exact types for TypeScript currently, but you can create a simple type check to enforce strict object spread:
// returns T, if T and U match, else never
type ExactType<T, U> = T extends U ? U extends T ? T : never : never
const bar: Bar = { a: "a string", b: 1 };
const foo: Foo = { a: "foofoo" }
const fooMergeError: Foo = { ...bar as ExactType<typeof bar, Foo> }; // error
const fooMergeOK: Foo = { ...foo as ExactType<typeof foo, Foo> }; // OK
With a helper function, we can reduce redundancy a bit:
const enforceExactType = <E>() => <T>(t: ExactType<T, E>) => t
const fooMergeError2: Foo = { ...enforceExactType<Foo>()(bar) }; // error
Code sample