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
  • 1
    \$\begingroup\$Using the ES6 Map could help with some stuff.\$\endgroup\$
    – gcampbell
    CommentedJul 6, 2016 at 17:08

2 Answers 2

7
\$\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\$
1
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\$
1
  • \$\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\$CommentedJul 8, 2016 at 6:51

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.