This looks like a bug or limitation in TypeScript; see microsoft/TypeScript#37103 (labeled a bug) or microsoft/TypeScript#32236 (labeled "needs investigation").
When you spread properties into an object literal and then add a computed property, it seems that TypeScript will completely ignore the computed property unless the computed key is of a single, specific, literal type (not a union of literals, and not a generic type parameter constrained to a string literal):
function testing<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
const someObject = { a: "v" } // { a: string }
const objA = { ...someObject, [a]: 0 } // { a: number }
const objX = { ...someObject, [x]: 0 } // { a: string }
const objY = { ...someObject, [y]: 0 } // { a: string }
const objZ = { ...someObject, [z]: 0 } // { a: string }
}
Computed property keys in general are a bit problematic in TypeScript, even without spreading, as they tend to get widened all the way to string, losing information you might care about (see another bug at microsoft/TypeScript#13948):
function testing2<K extends "a" | "b">(a: "a", x: string, y: "a" | "b", z: K) {
const objA = { [a]: 0 } // { a: number }
const objX = { [x]: 0 } // { [x: string]: number; } ♂️
const objY = { [y]: 0 } // { [x: string]: number; } ♂️
const objZ = { [z]: 0 } // { [x: string]: number; } ♂️
}
So that's the problem you're having here.
I don't know what to say other than "tread lightly around computed properties". You can kind of work around it by defining your own function that produces a more "correct" version of what a computed property would be:
function kv<K extends PropertyKey, V>(k: K, v: V) {
return { [k]: v } as { [P in K]: { [Q in P]: V } }[K]
}
const k = Math.random() < 0.5 ? "a" : "b";
const obj = kv(k, Math.random());
/* const obj: {
a: number;
} | {
b: number;
} */
You can see that a union of keys produces a union of objects. But if the key is generic then the compiler leaves that unevaluated and spread with generics tends to be represented as an intersection, which is often acceptable but not when keys overlap. (See microsoft/TypeScript#32022 for that issue):
function setA2<K extends keyof A>(k: K, obj: A): A {
return {
...obj,
...kv(k, '933')
}
/* const ret1: A & { [P in K]: { [Q in P]: string; }; }[K] */ // no error
}
So you'd have to widen k from K to keyof A before you got any warning:
function setA3<K extends keyof A>(k: K, obj: A): A {
const kW: keyof A = k;
return {
...obj,
...kv(kW, '933')
}; // FINALLY AN ERROR
// Type '{ a: string; b: "456"; } | { b: string; a: "123"; }' is not assignable to type 'A'.
}
So, hooray, I guess... you can spend a lot of effort to wrestle some type safety out of this function, but it's quite a pyrrhic victory (I think that's a word). So again, be careful around computed properties until and unless the relevant GitHub issues are fixed.
Playground link to code