Reputation: 155
Why does TypeScript accept the definition of seta
when it does not return objects of type A
?
type A = {
a: '123',
b: '456'
}
// Returns copy of obj with obj[k] = '933'
function seta<K extends keyof A>(k: K, obj: A): A {
return {
...obj,
[k]: '933'
}
}
const w: A = seta('a', {
a: '123',
b: '456'
})
// now w = {a: '933', b: '456'}
Upvotes: 8
Views: 1033
Reputation: 328618
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.
Upvotes: 3
Reputation: 1398
It's accepted because if you hover on [k]: '933',
it says
(parameter) k: K extends keyof A
Which means you're returning a property that's extended from the return type, which is allowed.
When you declare that an object must be of an interface, you are saying that it must have at least those properties, not only those properties.
Upvotes: 1