I hope this is not a silly question. Why when I use for (let property in myObject)...
property is recognized by typescript as "string"
instead of "keyof typeof myObject"
? Is there a way or setting to use the for...in to iterate over an object in a typesafe way?
Important: this question is very similar to this one, I understand if it might be considered a duplicate. I decided to post a new question because the question is quite old, and it does not addresses the case of a constant object or with a defined type declared as const
. Many of its answers address the usage of variable objects or arrays, and only one (the most recent one) addresses iterating over an object like I need. But I don't know how his answer, compared to the alternatives I was trying to implement compare, so I wanted to focus on this specific use case.
For example, this is perfectly valid code in Javascript
let myObject = {
a: "propA",
b: "propB"
}
for (let prop in myObject){
console.log(`${prop}: ${myObject[prop]}`);
// prints "a: propA", "b: propB"
}
But in typescript its marked as invalid, and throws the following error: "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ a: string; b: string; }'."
I don't understand why. As I understand it, prop
should be typed as "a" | "b" because the for...in iterates over the properties of an object, so the type system should expect no other values going there.
I read this SO post which explain that because the object accessor can be pretty much anything, then typescript types it as string, but if I have a constant, or declare it as const I expect the properties to be inferred, yet it makes no difference. I don't know if I am missing something
That post also has one answer that states that to use for...in you can use Object.prototype.hasOwnProperty.call(obj, key)
to validate if the property exists, but I don't know if this is the best way and also it looks kind of redundant.
I tried declaring a type beforehand as type myObjectProps = keyof typeof myObject;
but I found that "The left-hand side of a 'for...in' statement cannot use a type annotation."
The cleanest solution I found is to cast the property as follows
for (let prop in myObject){
console.log(`${prop}: ${myObject[prop as myObjectProps]}`)
}
But I don't know if its the proper way to do it, or if its something safe to do in the first place, or there is in fact a situation that can happen in which a prop will come from myObject but will not be accessible through object[prop]
?
While it's clear why the key is string
(types in TS are open, so at runtime could be polluted unexpectedly, even I polluted DOM prototypes with iterable properties), you could use a branded type to make the keys more specific, but you could still encounter runtime errors.
As a result TS tells you that iterating keys isn't safe in general and that's a good thing.
type Sealed = { _sealed?: never };
interface ObjectConstructor {
seal<T>(obj: T): asserts obj is T & Sealed;
iterateKeys<T>(obj: T): T extends Sealed ? IterableIterator<Exclude<keyof T, '_sealed'>> : IterableIterator<string>;
}
Object.defineProperty(Object, 'iterateKeys', {
value: function* (obj: object) {
for (const key of Object.keys(obj)) {
yield key;
}
}
});
// Usage
const myObject = { key1: 1, key2: 2 };
// Type before sealing
for (const prop of Object.iterateKeys(myObject)) {
// `prop` is string
console.log(`${prop}: ${myObject[prop as keyof typeof myObject]}`);
}
Object.seal(myObject);
// Type after sealing
for (const prop of Object.iterateKeys(myObject)) {
// `prop` is now inferred as keyof typeof myObject
console.log(`${prop}: ${myObject[prop]}`);
}
function makeSomething<T extends typeof myObject>(obj: T){
Object.seal(obj);
for(const prop of Object.iterateKeys(obj)){
obj3[prop] = 2// OOPS const prop: "key1" | "key2"
}
}
const obj2 = {key1: 1, key2: 2, key3:3};
const obj3 = {key1: 1, key2: 2};
Object.seal(obj3);
makeSomething(obj2);