3
\$\begingroup\$

Exercise from a TypeScript course:

Create a map using TypeScript Generics. The key is always a string.

The value can be a string or a number. It has to work with both types.

It's got to have the following methods:

  • Creating a new key-value pair : setItem(key: string, item: T)

  • Retrieving values : getItem(key: string)

  • Remove all items : clear()

  • Display all key-value items : printMap() "

Here my uncompiled TypeScript code:

// -- THE ACTUAL IMPLEMENTATION --------------------------
class GenericMap < T extends number | string > {
  private items: object[]

  constructor() {
    this.items = [];
  }

  setItem(key: string, item: T): void {
    let tmp = {};
    tmp[key] = item;

    this.items.push(tmp);
  }

  getItem(key: string): T {
    let ret = null;

    for (let i = 0; i < this.items.length; i++) {
      let curr = this.items[i];

      if (curr[key]) {
        return curr[key];
      }
    }

    return null;
  }

  clear() {
    this.items.length = 0;
  }

  printMap() {
    this.items.forEach((item) => {
      let key: string[] = Object.keys(item);

      console.log('{ "' + key[0] + '": ', item[key[0]], '}');
    });
  }
}

// ------- JUST TESTING --------------------
const numb = new GenericMap < number > ();
numb.setItem('alpha', 7);
numb.setItem('beta', 14);
numb.printMap();
numb.clear();
numb.printMap();

const strMap = new GenericMap < string > ();
strMap.setItem('color', 'Blue');
strMap.setItem('height', '54 cm');
strMap.printMap();
// ---------------------------------------------

The compiled code as a live-demo:

class GenMap {
  constructor() {
    this.items = [];
  }
  setItem(key, item) {
    let tmp = {};
    tmp[key] = item;
    this.items.push(tmp);
  }
  getItem(key) {
    let ret = null;
    for (let i = 0; i < this.items.length; i++) {
      let curr = this.items[i];
      if (curr[key]) {
        return curr[key];
      }
    }
    return null;
  }
  clear() {
    this.items.length = 0;
  }
  printMap() {
    this.items.forEach((item) => {
      let key = Object.keys(item);
      console.log('{ "' + key[0] + '": ', item[key[0]], '}');
    });
  }
}
const numb = new GenMap();
numb.setItem('alpha', 7);
numb.setItem('beta', 14);
numb.printMap();
numb.clear();
numb.printMap();
const strMap = new GenMap();
strMap.setItem('color', 'Blue');
strMap.setItem('height', '54 cm');
strMap.printMap();

It works fine. But I guess there a better ways to solve these exercise.

Especially my getItem-method has become rather long.

Has someone an idea to solve the exercise in a more elegant way?

What do you think about my idea of storing objects within an array?

Looking forward to reading your answers and comments.

\$\endgroup\$
2
\$\begingroup\$

It took me a while to understand the way getItem() works in its current implementation. I even thought that there's a bug (after rereading the code, I see I was wrong about it).

  • Let's start with obvious. The ret variable; can be safely removed.

  • The whole search loop is counter-intuitive to me, because each element in the items array is an object which stores a value: T under a different key: string. If you really want to use items: object[], you'd rather have it's elements declared as { key: string, value: T }. And this immediately moves us to the next point that answers your question about the choice of the type of the backing object.

  • An entity that stores key-value pairs in TypeScript and JavaScript is Object. Why not use it instead? I don't know if using Object is disallowed by your real problem definition.

    If it is not disallowed, it would be very natural to define items as an Object and wrap it into a GenericMap which exposes the API your problem requires.

    However, if it is disallowed, you should not use it in the setItem()'s code (tmp[key] = item;) as well. Because it's technically same kind of a "cheat" anyway.

Below is the code that is built around an Object instead of an Array. As you can see, pretty much all of the operations are now "pass-through" one liners. Including the getItem() which you said got pretty long in the original code.


class GenericMap<T extends number | string> {
  private items: { [key: string]: T } = { };

  setItem(key: string, item: T): void {
    this.items[key] = item;
  }

  getItem(key: string): T {
    return this.items[key];
  }

  clear(): void {
    this.items = {};
  }

  printMap(): string {
    const itemsAsString = Object
      .getOwnPropertyNames(this.items)
      .map(key => `'${key}': ${this.getItem(key)}`)
      .join(", ");
    return `{ ${itemsAsString} }`;
  }
}

A few TypeScript style side notes:

  • private items: { [key: string]: T } = { };
    • If we initialize our fields in-place (as a part of their definition), we don't need an explicit constructor() which makes code shorter.
    • { [key: string]: T } is a compiler hint that will -- at least in some cases -- help detecting incorrect usages of the field
  • I personally have never seen the whitespaces around < and > when used with generics, e.g. GenericMap < T extends number | string >. It's not TypeScript idiomatic I believe.

Hope, it helps.

\$\endgroup\$
  • \$\begingroup\$ One more thing to mention, enabling strictNullChecks is always a good idea. \$\endgroup\$ – Gerrit0 Oct 23 '17 at 23:27
  • \$\begingroup\$ (As I realized this wouldn't point out the possible error in this code, this.items[key]; can be undefined and should be typed as such) \$\endgroup\$ – Gerrit0 Oct 23 '17 at 23:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.