HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptMinor

TypeScript Dictionary class

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
dictionarytypescriptclass

Problem

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

Solution

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 {
  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 implements Dictionary {

  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();

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();
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.

Code Snippets

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;
  }

}
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

Context

StackExchange Code Review Q#134030, answer score: 7

Revisions (0)

No revisions yet.