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'}
https://tsplay.dev/wEGX4m
Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings. index.ts.
Define a Type for Object with Dynamic keys in TypeScript# Use an index signature to define a type for an object with dynamic keys, e.g. [key: string]: string;. Index signatures are used when we don't know all of the names of a type's properties ahead of time, but know the shape of the values.
The type of myVar is a string, and not all strings are properties of the object, so TypeScript informs us that we can't safely access the property dynamically. If you try to set the type of myVar to be a union of the object's keys, you would still get an error. The easiest way to get around this is to use a type assertion.
Checking the type of an object in Typescript: the type guards. Coming from a JS background, checking the type of an object in Typescript is kind of obscure at first. We are used to if (obj.property) {//obj.property exists here !} and this is not possible in Typescriptβ¦. In some cases, you can use type guards.
Use bracket notation to access the object's property, e.g. obj [myVar]. The keyof typeof syntax allows us to get a union type of the object's keys. This way we can inform TypeScript that the myVar variable will only ever store a string that is equal to one of the keys in the object. Now we can access the object property dynamically.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With