3
\$\begingroup\$

I recently implemented a dictionary class in TypeScript. I'm still new to JavaScript so I'm curious what people think about it.

class Dictionary {
    public constructor ();
    public constructor (object: Object);
    public constructor (object?) {
        if (object != null) {
            for (let property in object) {
                if (object.hasOwnProperty(property)) {
                    this[property] = object[property];
                }
            }
        }
    }

    public clone(): Dictionary {
        let result = new Dictionary(this);
        return result;
    }

    public getKeys(): string[] {
        let result = [];
        for (let item in this) {
            if (this.hasOwnProperty(item)) {
                result.push(item);
            }
        }
        return result;
    }

    public getValues(): any[] {
        let result = [];
        for (let item in this) {
            if (this.hasOwnProperty(item)) {
                result.push(this[item]);
            }
        }
        return result;
    }

    public tryAddItem(key: string, value: any): boolean {
        let isAddItem = !this.hasOwnProperty(key) && typeof(value) !== 'undefined';
        if (isAddItem) {
            this[key] = value;
        }
        return isAddItem;
    }

    public tryUpdateItem(key: string, value: any): boolean {
        let isUpdateItem = this.hasOwnProperty(key) && typeof(value) !== 'undefined';
        if (isUpdateItem) {
            this[key] = value;
        }
        return isUpdateItem;
    }

    public tryDeleteItem(key: string): boolean {
        let isDeleteItem = this.hasOwnProperty(key);
        if (isDeleteItem) {
            delete this[key];
        }
        return isDeleteItem;
    }
}

I realized that objects in JavaScript act a lot like dictionaries so I'm basically just adding functionality to the object. I'm not sure how I would want to handle sorting. I think I would prefer to create a method that returned a sorted array or object based on some function. Similar to what a JavaScript Linq library would do.

\$\endgroup\$
  • 1
    \$\begingroup\$ Using the ES6 Map could help with some stuff. \$\endgroup\$ – gcampbell Jul 6 '16 at 17:08
6
\$\begingroup\$

Edit: I've updated the code and uploaded a gist that will work in the TS playground - the gist will compile with Typescript 3.1.

The primary benefit of Typescript is to provide types for Javascript. And since Typescript provides Generics - I don't think your implementation of a Dictionary provides any benefit over a plain javascript object.

In fact, as you noted in the comments, JS Objects (ie TS object) are dictionaries. You just need to provide types around them for your dictionary. (btw - I also come from a C# background, but I've also come to love the underlying functional nature of JS that TS lets us type).

Here's a code sample of the direction I would expect a Dictionary.ts class to have in a code base I work in. Now that we have Mapped Types, we can generalize dictionaries better.

export interface Dictionary<K, V> {
  getKeys(): K[];
  getValues(): V[];
  get(key: K): V | null; // the key might not exist
  put(key: K, val: V): void; // or boolean?
}


export class JSDictionary<K extends string, V> implements Dictionary<K, V> {

  private internalDict: { [key in K]?: V };

  constructor() {
    this.internalDict = {};
  }

  public getKeys() {
    let keys: K[] = [];
    for(let key in this.internalDict) {
      keys.push(key);
    }

    return keys;
  }

  // Type predicate to ensure v exists
  private exists(v: V | undefined): v is V {
    return v != null && typeof v !== "undefined";
  }

  public getValues() {
    let vals: V[] = [];

    for(let key in this.internalDict) {
      let v = this.internalDict[key];
      if(this.exists(v)) {
        vals.push(v);
      }
    }

    return vals;
  }

  public get(key: K) {
    let v = this.internalDict[key];
    return this.exists(v)
      ? v
      : null;
  }

  public put(key: K, val: V): void {
    this.internalDict[key] = val;
  }

}

Example Usage

type myKeys = 'FOX' | 'CAT' | 'DOG';

interface Animal {
  species: string;
  name: string;
  weight: number;
}

// A dictionary that hols one fox/cat/dog.
let myAnimalPen = new JSDictionary<myKeys, Animal>();

myAnimalPen.put('FOX', { name: 'Foxworth', species: 'Fox', weight: 40 });

// a dictionary that takes any string and maps it to a number
let idDict = new JSDictionary<string, number>();
idDict.put('somehas', 1204);
idDict.put('yeahaasd', 3306);

let yeaID = idDict.get('yeahaasd'); // yeaID is a number | null type
let myFox = myAnimalPen.get('FOX'); // myFox is an Animal | null type

Major points:

  • Code to an interface as much as you can in typescript. Affords you flexibility
  • Generic types
  • Transparent use of Plain JS object as the underlying dictionary (JS Engines optimize this very use case!)
  • I removed overloaded constructors, but you could add that back in with the proper types and it will work as expected
  • Left out clone - pretty easy to implement though
  • No exception handling when get returns null, like your 'try*' functions. I actually like try functions like you have, so you could mostly copy/paste, although with the stricter typings sometimes that will be taken care of by the compiler for you.
\$\endgroup\$
3
\$\begingroup\$

From a short review:

  • I would follow the method names provided at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map they are both shorter and more intuitive. Your method names are too long

  • More particularly, I would change the name of 'getValuestoclone`

  • I would have the set/get/delete method return the Dictionary object for chained calls, if set/get is called on a ownProperty, I would throw an exception.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the review! I intentionally left out the get and set because Dictionary[key]; gets the value and Dictionary[key] = vlaue; sets the value. I'm also not a fan of failing javascript. This is why I have the tryAdd, tryUpdate, and tryDelete. Although, I like the idea of chaining but I think this would go against the Single Responsibility rule in SOLID and the Command-Query separation principle. I think a type of linq library that could be used with Dictionaries should support chaining. Although, that's definitely me speaking from a C# bias. :) \$\endgroup\$ – christo8989 Jul 8 '16 at 6:51

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.