There are a few open issues regarding the treatment of computed property keys in TypeScript that would need to be addressed to give you the expected behavior. One is microsoft/TypeScript#36920 in which computed properties are never treated as excess properties, and another is microsoft/TypeScript#38663 in which computed keys of union type end up being ignored entirely if they partially conflict with known property keys. Both of these issues are considered feature requests (and not bugs, even though the latter one leads to unsoundness).
Excess property checks are more of a linter feature than a type safety feature, and don't get applied everywhere. In some sense they are at odds with a fundamental property of the TypeScript type system: when you extend a type by adding extra properties, the new type is still structurally compatible with the old type. Excess properties therefore must be allowed in many circumstances. They are only considered errors in specific scenarios involving object literals, and apparently computed properties are not one such scenario. Maybe they should be, as microsoft/TypeScript#36920 suggests, but they aren't. So in your code, the possibility that random
is added as a key doesn't raise any warnings. Oh well. That explains why { valid?: string, random: number }
is allowed.
Potentially more problematic is the fact that computed property keys of a union type get widened all the way to a string
index signature (see microsoft/TypeScript#13948), and then silently dropped when combined with other properties. So the type of const objB = { ...objA, [specKey]: 123123213 }
is just { valid?: string | undefined; }
and not the expected { valid?: string, random: number } | { valid: number }
. We know that { valid?: string, random: number }
is structurally compatible with TestObj
, but { valid: number }
is certainly not. Nowhere are we warned that we might be assigning a number
to a property that expects a string
. That's what microsoft/TypeScript#38663 suggests. If you want to see that happen, you might want to go to that issue and give it a . Until and unless that's implemented, you'll need to work around it.
One workaround I tend to use for the index signature issue is to wrap computed property key creation in a helper function that gives the typings I expect. See this comment on microsoft/TypeScript#13948. Like this, for instance:
function kv<K extends PropertyKey, V>(k: K, v: V): { [P in K]: { [Q in P]: V } }[K] {
return { [k]: v } as any
}
Then instead of {[k]: v}
in a literal, I write {...kv(k, v)}
:
function testFunc(key: SpecKey) {
const objC: TestObj = { ...objA, ...kv(key, 123123123) }; // error!
// Type '{ valid: number; } | { random: number; valid?: string | undefined; }'
// is not assignable to type 'TestObj'
}
Now you get the error you expect, specifically that you are trying to assign a value of type { valid: number; } | { random: number; valid?: string }
to a variable of type { valid?: string }
, which is not allowed, because number
is not a string
.
Playground link to code