Home > Software design >  Easiest way to create a Hash of Arrays (or equivalent) in Typescript?
Easiest way to create a Hash of Arrays (or equivalent) in Typescript?

Time:12-13

I am looking to create a Hash of Arrays (or some equivalent structure) that allows me to collect an unknown set of properties (keyed by name) and have each property store an array of things that claimed they have said property.

const currentProperties = currentObject.getProperties(); 
// we can assume getProperties correctly returns an array of valid properties

currentProperties.forEach( (v) => {  
  HoA[ v ].push( currentObject );  
});

I want to be able to do something like the above to populate the Hash of Arrays - but how to I actually properly initialize it/do all of the TypeScript stuff? Currently I've been getting by using an enum to manually specify the possible properties that could show up, but I want to adapt it out to a structure that doesn't need to have a property list ahead of time, and can just take whatever shows up as a key.

As noted above, I understand how to solve a version of this problem if I manually specify the expected types of properties to be seen and use a bunch of

if (currentProperties.includes(Properties.exampleOne)) {
  this.exampleGroupOne.push(currentObject);
}

but I want to be able to have this work with no prior knowledge of what values of properties exist.

EDIT: some clarification on what I am asking for -

The goal is to have a bunch of objects that have a getProperties() method that returns an array of zero or more attributes. I want to have a data structure that, for each attribute that exists, ends up with an array of the objects that reported that attribute. That is easy when I know the possible attributes ahead of time, but in this case, I won't. For actually acting on the attributes, I'll need a loop that is the attributes on the outer layer [the hash] and the relevant objects on the inner layer [the array]. (This is why I'm assuming HoA)

EDIT #2:

class Alice {

    myProps(): string[] {
        return ["Apple"];
    }
}

class Bob {

    myProps(): string[] {
        return ["Banana"];
    }
}

class Charlie {

    myProps(): string[] {
        return ["Apple", "Banana"];
    }
}

const FruitBasket:{ [prop: string]: string} = {}

const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();

const Objects = [myAlice, myBob, myCharlie];

for (const currentObject of Objects) {
    const fruits = currentObject.myProps();
    fruits.forEach( (v) => { FruitBasket[v].push(currentObject);});
}

I think this is almost what I want - I am getting an error that push does not exist on type string, but at this point I think I'm just missing something basic because I've been staring at this too long.

EDIT #3:

abstract class JustSomeGuy {
    myProps(): string[] {
        return [];
    }
    myName(): string {
        return '';
    }
}

class Alice extends JustSomeGuy {

    myProps(): string[] {
        return ["Apple"];
    }
    myName(): string {
        return 'Alice';
    }
}

class Bob extends JustSomeGuy {

    myProps(): string[] {
        return ["Banana"];
    }
    myName(): string {
        return 'Bob';
    }
}

class Charlie extends JustSomeGuy {

    myProps(): string[] {
        return ["Apple", "Banana"];
    }
    myName(): string {
        return 'Charlie';
    }
}

const FruitBasket:{ [prop: string]: JustSomeGuy[]} = {}

const myAlice = new Alice();
const myBob = new Bob();
const myCharlie = new Charlie();

const Objects = [myAlice, myBob, myCharlie];

for (const currentObject of Objects) {
    const fruits = currentObject.myProps();
    fruits.forEach( (v) => { (FruitBasket[v] ??= []).push(currentObject);});
}

for (let key in FruitBasket){
    let value = FruitBasket[key];
    for (let i = 0; i < value.length; i  ){
        console.log("In key: "   key   " the ith element [i = "   i   "] is: "   value[i].myName() );
    }
}

I believe that this is what I want. Marking this as resolved.

CodePudding user response:

Let's start with the types of the data structures that you described:

type ObjWithProps = {
  getProperties (): string[];
};

type PropertyHolders = {
  [key: string]: ObjWithProps[] | undefined;
};
// Could also be written using built-in type utilities like this:
// type PropertyHolders = Partial<Record<string, string[]>>;
  • The type ObjWithProps has a method which returns an array of string elements.

  • The type PropertyHolders is an object type that is indexed by string values (keys), and each value type is an array of ObjWithProps (if it exists, or undefined if it doesn't) — no object has a value at every possible key.

Next, let's replicate the data structures you showed in your example:

const HoA: PropertyHolders = {};

const currentObject: ObjWithProps = {
  getProperties () {
    return ['p1', 'p2', 'p3' /* etc. */];
  }
};

const currentProperties = currentObject.getProperties();

In the code above, the currentObject has some arbitrary properties (p1, p2, p3). This is just to have reproducible example data. Your own implementation will likely be different, but the types are the same.

Finally, let's look at the part where you assign the values to the hash map:

currentProperties.forEach((v) => {
  HoA[v].push(currentObject); /*
  ~~~~~~
  Object is possibly 'undefined'.(2532) */
});

You can see that there's a compiler error where you try to access the array at the key v. Because you aren't sure that the array exists (no object has a value at every key), trying to invoke a push method on undefined would throw a runtime error. TypeScript is trying to help you prevent that case.

Instead, you can use the nullish coalescing assignment operator (??=) to ensure that the array is created (if it doesn't already exist) before pushing in a new value. This is what that refactor would look like:

currentProperties.forEach((v) => {
  (HoA[v] ??= []).push(currentObject); // ok
});

Full code in TS Playground


Utility types references:

  • Related