Reputation: 111
I have a JSON object imported from a JSON file (with resolveJsonModule: true
).
The object looks like this:
"myobject": {
"prop1": "foo",
"prop2": "bar"
}
and it's type therefore looks like this:
myobject: { prop1: string, prop2: string }
That's very nice but when I try to use a for...in
loop,
for (const key in myobject) {
console.log(myobject[key])
}
I get this error:
TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ "prop1": string; "prop2": string; }'.
No index signature with a parameter of type 'string' was found on type '{ "prop1": string; "prop2": string; }'.
I understand that this means the iterator key
is of type string
and not of type 'prop1' | 'prop2'
. But I don't understand why the iterator doesn't get this type because I'm explicitly iterating through the property names of myobject
. Did I miss a tsconfig property that enables this behavior?
I would like not to do this:
for (const key in myobject) {
console.log(myobject[key as 'prop1' | 'prop2'])
}
Because:
Upvotes: 8
Views: 6617
Reputation: 1
I don't understand why the iterator doesn't get this type because I'm explicitly iterating through the property names of myobject. Did I miss a tsconfig property that enables this behavior?
That's because you can extend an object property and typescript will still consider it a type of the same object. E. g:
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for (const k in abc) { // const k: string
const v = abc[k];
// ~~~~~~ Element implicitly has an 'any' type
// because type 'ABC' has no index signature
}
}
const x = {a: 'a', b: 'b', c: 2, d: new Date()};
foo(x); //OK, no errors
That's because ts considers x to be an ABC type because it has the properties a, b and c.
To fix the error in the function foo, you need to say that k is of the type of keyof ABC.
function foo(abc: ABC) {
let k: keyof ABC;
for (k in abc) { // let k: "a" | "b" | "c"
const v = abc[k]; // Type is string | number
}
}
Upvotes: 0
Reputation: 74490
Three solutions for typing for...in
loops, I am aware of:
A type assertion will force key
type to be narrowed to myobject
keys:
for (const key in myobject) {
console.log(myobject[key as keyof typeof myobject])
}
The key variable cannot be typed inside the for-in loop, instead we can declare it outside:
let key: keyof typeof myobject // add this declaration
for (key in myobject) {
console.log(myobject[key]) // works
}
function foo<T>(t: T) {
for (const k in t) {
console.log(t[k]) // works
}
}
foo(myobject)
key
in a for...in
loop will by design default to type string
. This is due to the structural type system of TypeScript: the exact properties' keys shape is only known at run-time, the compiler cannot statically analyze, what properties are present on the object at compile-time. A key
type narrowed to myobject
properties would make the for...in
loop an unsafe operation type-wise.
Note: Some linked resources discuss Object.keys
, for which the same argumentation holds.
Why doesn't Object.keys
return a keyof type in TypeScript? - by Ryan Cavanaugh
Comment by Anders Hejlsberg in TypeScript#12253 - also mentions for...in
TypeScript#32321 links to a multitude of duplicate issues
Specific comment towards for...in
by Anders Hejlsberg:
I have my doubts about this one. In
for (var k in x)
wherex
is of some typeT
, it is only safe to say thatk
is of typekeyof T
when the exact type ofx
isT
. If the actual type ofx
is a subtype ofT
, as is permitted by our assignment compatibility rules, you will see values ink
that are not of typekeyof T
.
Upvotes: 19
Reputation: 2632
A better way to this is:
for (const key in myobject) {
console.log(myobject[key as keyof typeof myobject])
}
In this way, it won't break when you add a property or rename it
Upvotes: 7
Reputation: 971
if you want to have an object to be dynamic in the future create a model like this
interface PropertyItemModel {
propName: string;
propValue: string;
}
and in the component you can fetch data by loop
export class AppComponent {
items: PropertyItemModel[] = [];
constructor() {
this.items = [
{ propName: "1", propValue: "foo" },
{ propName: "2", propValue: "bar" }]
this.items.forEach(item => {
console.log(`name: ${item.propName} - value: ${item.propValue}`)
});
}
}
Upvotes: 2