This has all to do with how the spread is resolved with generic types (see PR) compared to normal types. If you write the resulting object to a variable, you will immediately notice the difference: for non-generic types, the merged type is inferred as:
{
profile: {};
userId: number;
}
which leads to this type being unassignable to the annotated return type User that has a required username subproperty. This is exactly what the compiler error TS 2322 is telling you:
Property 'username' is missing in type '{}' but required in type '{ username: string; }'
Now, the situation with generics is a bit different: the type is actually inferred as an intersection of a subtype of User and a { profile: {}; } type:
T & {
userId: string;
profile: {};
}
The compiler is ok with that as the intersection is an "extension" of the annotated return type containing all the properties as per the definition of the intersection.
Whether this a good behavior is debatable as you can do the following, and the compiler will be none the wiser:
function updateUsernameGeneric<T extends User>(user: T): T {
const newUser = {
...user,
userId: "234",
profile: {
// No error (unexpected... why?)
}
}
return newUser;
}
updateUsernameGeneric({ profile: { username: "John" }, userId: 123 }).userId //a-ok, "number"
Since the return type is an intersection of the generic type parameter and the merged properties, you can de-annotate the return type and let TypeScript infer it. Incompatible property types will be correctly inferred as never:
function updateUsernameGenericFixed<T extends User>(user: T) {
const newUser = {
...user,
userId: "234",
profile: {
// No error (unexpected... why?)
}
}
return newUser;
}
updateUsernameGenericFixed({ profile: { username: "John" }, userId: 123 }).userId //never
Playground