1

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]?

2
  • 4
    Nothing has really changed since that question. Objects declared as const don't really make the object immutable or sealed, which is what you'd need for your code to be safe. This is the subject of ms/TS#31652. Does that fully address your question? If so I'll write an answer or close as a duplicate. If not, what am I missing? Commented May 14 at 3:46
  • tsplay.dev/WJdElN See this which has a "const object" and yet manages to go over extra properties. Nothing has changed - TS doesn't operate at runtime, the for..in loop will not go over properties known to TS, but over all properties. TS further doesn't track "immutableness" - not that as const makes something immutable by itself. But even if you freeze or seal an object, that won't be tracked in the type. Commented May 14 at 4:45

1 Answer 1

2

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.

Playground

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);
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.