Such an intriguing question! As per this specific issue, the spread operator's purpose is to intentionally avoid triggering excess property checks.
// Despite barObject having more properties than Foo, no excess property checks are triggered by the spread
const fooObject: Foo = { ...barObject };
// 'b' being an explicit property triggers checks in this case
const foo: Foo = { ...bar, b: 2 }
Presently, TypeScript lacks exact types, although implementing a basic type check can help enforce strict object spreading:
// If T and U match, returns T; otherwise, returns 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
To minimize redundancy, we can utilize a helper function:
const enforceExactType = <E>() => <T>(t: ExactType<T, E>) => t
const fooMergeError2: Foo = { ...enforceExactType<Foo>()(bar) }; // error
Explore a sample code snippet here