It's a known limitation of TypeScript that there is no direct type operator like `{...T, ...U}` to handle object spread with overwritten properties. You can check out the feature request regarding this at microsoft/TypeScript#10727, along with related issues such as microsoft/TypeScript#50185 and microsoft/TypeScript#50559.
When spreading objects of specific types, TypeScript prevents overwritten properties in the result:
const specific = { ...{ a: 1, b: "two" }, b: 2 };
/* const specific: {
b: number;
a: number;
} */
However, when dealing with values of generic types, TypeScript approximates the result as an intersection type, as highlighted in microsoft/TypeScript#28234:
function generic<T extends { a: number, b: string }>(t: T) {
return { ...t, b: 2 };
}
/* function generic<T extends { a: number; b: string;}>(
t: T
): T & { b: number; }
*/
As explained in microsoft/TypeScript#28234,
The use of intersections seems to be the best approach for cases involving objects with overlapping property names and different types, balancing accuracy and complexity effectively.
In your code, the expression { ...item, id: newId }
is considered to have the intersection type
T & {id: string | undefined}</code, making it assignable to <code>T
:
const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
const ret = { ...item, id: newId };
// const ret: T & { id: string | undefined; }
return ret;
}
To address this in your example code, you can use a type assertion to derive a more accurate type using the Omit
utility type:
const replaceId = <T extends MaybeHasId>(item: T, newId?: string) => {
const ret = { ...item, id: newId };
// const ret: T & { id: string | undefined; }
return ret as Omit<T, "id"> & { id: string | undefined }
}
This adjustment will lead to the expected error prompt:
const moreSpecificObject: HasId = replaceId({ id: "specific id" }, undefined);
// -> ~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'.
Link to play around with the code on the TypeScript Playground