Marcel
Marcel

Reputation: 1011

How to iterate over keys of a generic object in TypeScript?

I need to iterate over a large object which is just typed as "object". It contains an unknown number of objects of the same type.

In older posts I had found solutions using a generator within a custom Symbol.iterator function to make the large object iterable with a for..of loop.

But it seems to me, now in 2017, just using Object.keys is actually easier:

Object.keys(bigObject).forEach((key:string)=>{
console.log(bigObject[key]);
});

This actually runs just fine! But the TypeScript compiler keeps giving me the error "error TS7017: Element implicitly h as an 'any' type because type '{}' has no index signature"

Does anybody have an idea what I am missing here? Or what is the current best practice to do such an iteration with ES2015 and TypeScript (2.2.2)?

Upvotes: 88

Views: 184798

Answers (7)

Paleo
Paleo

Reputation: 23692

A dictionary type

It contains an unknown number of objects of the same type.

Maybe with a generic interface BigObject<T> for your dictionary?

interface BigObject<T> {
    [index: string]: T
}

let bigObject: BigObject<object> = {}
Object.keys(bigObject).forEach(key => {
  console.log(bigObject[key])
})

Here I wrote the type object in let bigObject: BigObject<object>. You can use a better type.

Notice about the for .. in solution

I've noticed that other answers suggest a for .. in loop. This was the only way to iterate through the properties of an object in the 20s, and somehow it makes me nostalgic. It's nice to use these old tricks. However, there's a little pitfall to avoid. It is mandatory to use Object.hasOwn() (the replacement of hasOwnProperty). Here is the correct way to use a for .. in loop:

for (const propName in obj) {
  if (!Object.hasOwn(obj, propName)) continue;
  // ...
}

See also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#iterating_own_properties

To illustrate the problem, let's imagine that somewhere in a library you are importing is this kind of code :

Object.prototype.extraProperty = "This is inherited";

… then all your for .. in loops will iterate on the inherited extraProperty too.

One last thing to know: as far as performance is concerned, iteration on Object.keys is faster than a for .. in loop.

Upvotes: 30

Pall Arpad
Pall Arpad

Reputation: 1898

This is the closes thing I could come up in order to make the Typescript compiler happy

const genericObject: { [key: string]: any } = {    // any type can be replaced with union (if you need to be more specific)
  a: 1,
  b: 'test',
  c: false,
  d: { x: 1 },
  e: [1, 2, 3],
}


for (const key in genericObject) {
  testObj[key]
}

Upvotes: 0

RadicalMeme
RadicalMeme

Reputation: 1

If you want to get the keys of an object, use this.

for the longest time I always just put // eslint-disable-next-line no-type-assertion/no-type-assertion at the top of the line, used as WhateverType and called it a day.

using Hayden-Linder's suggestion i came up with something that finally makes ts happy.

export function getObjectKeys<T extends Object>(input: T) {
    let keys
    let key: keyof typeof input
    for (key in input) {
        if (!keys) {
            keys = [key]
        } else {
            keys.push(key)
        }
    }
    if (!keys) {
        return []
    }
    return keys
}

However with V 5.5 I think ts became slightly smarter and this has become obsolete.

Upvotes: 0

Luke Machowski
Luke Machowski

Reputation: 4211

for..in

When looking at the Typescript documentation (Typescript: Iterators and Generators), we see that the for..in syntax will iterate over the keys of the object.

for..in returns a list of keys on the object being iterated, whereas for..of returns a list of values of the numeric properties of the object being iterated.

We can use that to our advantage to index into our object and get the strongly typed value:

// Go through each key of the indexed object:
for (const key in indexedObject)
{
   // Get the indexed item by the key:
   const indexedItem = indexedObject[key];
   // Now we have the item.
   
   // Use it...
}

Solution

We can use that to get an elegant solution to the question:

// Go through each key in the bigObject:
for (const key in bigObject)
{
   // Get the strongly typed value with this name:
   const value = bigObject[key];
   // Now we have the the strongly typed value for this key (depending on how bigObject was typed in the first place).
   
   // Do something interesting with the property of bigObject...
}

As pointed out by jcalz (Thank you):

This solution works in JavaScript, but gives TypeScript errors when using --strict compiler options.

Upvotes: 42

Chris Chiasson
Chris Chiasson

Reputation: 816

Short Solution

For those who like to save on typing and avoid both the implicit any error and "the left hand side of a for...in statement must be a string" error, here is the shortest way I know of to iterate the properties of an object in Typescript. Note no interface or class.

export const collection = {property1: "", property2: ""};

let key: keyof typeof collection;
for (key in collection) collection[key] = key; // or other operation

Here we assign all the right hand sides of the properties to be some function of their key name--in this example, identity. Should compile in tsc with no errors.

Answering the Original Poster's Question

If you have a class instead of an object literal, you can use keyof followed by the class name. If you have some Object with unknown keys as the original poster asked about, you could try an inline type (see first line below):

export const collection: { [key: string]: any } = // inline type
  { Property1: "", Property2: "" }; // value could come from any source

let key: keyof typeof collection;
for (key in collection) collection[key] = key; // or other operation

Upvotes: 13

Hayden Linder
Hayden Linder

Reputation: 890

After playing with it a while I found a solution that makes the TS compiler happy. Declare the key outside the for loop:

type TObj = { 
  key1: string; 
  key2: string 
}

const obj: TObj = {
  key1: "foo",
  key2: "bar"
};

// Declare the key outside the for loop
let t: keyof TObj;

for(t in obj) {
  obj[t]; // no compiler error
}

Upvotes: 56

Carl G
Carl G

Reputation: 18250

Something like this may work:

type InferKey<T> = T extends Partial<Record<infer K, any>> ? K : never;
type InferValue<T> = T extends Partial<Record<any, infer V>> ? V : never;
/** Returns strongly-typed entries of obj. */
export const toEntries = <T extends Partial<Record<string, any>>>(obj: T) => {
  return Object.entries(obj) as [InferKey<T>, InferValue<T>][];
};


/** An example key type. */
export type JustificationFilterName =
  | "propositionId"
  | "propositionCompoundId"
  | "sourceExcerptParaphraseId"
  | "writQuoteId"
  | "writId"
  | "url";
export type JustificationFilters = Partial<
  Record<JustificationFilterName, string>
>;

/** Example usage: */
for (const [filterName, filterValue] of toEntries(filters)) {
  switch (filterName) {
    case "propositionId": {
      // ...
      break;
    }
    case "propositionCompoundId": {
      // ...
      break;
    }
    ...
    default:
      // filterName will have type `never` if your cases above are exhaustive,
      // and you can type newExhaustedEnumError to accept `never` so that TS
      // will catch non-exhaustive switch statements.
      throw newExhaustedEnumError(filterName);
  }
}

Upvotes: 1

Related Questions