Declare and initialize a Dictionary in Typescript
Asked Answered
N

8

429

Given the following code

interface IPerson {
   firstName: string;
   lastName: string;
}

var persons: { [id: string]: IPerson; } = {
   "p1": { firstName: "F1", lastName: "L1" },
   "p2": { firstName: "F2" }
};

Why isn't the initialization rejected? After all, the second object does not have the "lastName" property.

Nedi answered 8/4, 2013 at 10:56 Comment(3)
Note: this has since been fixed (not sure which exact TS version). I get these errors in VS, as you would expect: Index signatures are incompatible. Type '{ firstName: string; }' is not assignable to type 'IPerson'. Property 'lastName' is missing in type '{ firstName: string; }'.Huffy
Can you please update this post: the title doesn't align the with question and the accepted answer!Photomontage
The concept which allows us to make something like a dictionary in typescript is refered to as "Indexable Types" in the official typescript handbook (see Indexable Types ). Since it took me a while to find this out, I wanted to point everyone searching for the official documentation into the right direction by providing the "official name" for this feature.Joinery
D
455

Edit: This has since been fixed in the latest TS versions. Quoting @Simon_Weaver's comment on the OP's post:

Note: this has since been fixed (not sure which exact TS version). I get these errors in VS, as you would expect: Index signatures are incompatible. Type '{ firstName: string; }' is not assignable to type 'IPerson'. Property 'lastName' is missing in type '{ firstName: string; }'.


Apparently this doesn't work when passing the initial data at declaration. I guess this is a bug in TypeScript, so you should raise one at the project site.

You can make use of the typed dictionary by splitting your example up in declaration and initialization, like:

var persons: { [id: string] : IPerson; } = {};
persons["p1"] = { firstName: "F1", lastName: "L1" };
persons["p2"] = { firstName: "F2" }; // will result in an error
Disclamation answered 8/4, 2013 at 12:42 Comment(5)
Why do you need the id symbol? It seems it is unnecessary.Goines
Using the id symbol, you can declare what the type of the keys of the dictionary should be. With the declaration above, you couldn't do the following: persons[1] = { firstName: 'F1', lastName: 'L1' }Disclamation
the id symbol can be named anything you like and was designed that way to make it easier to read code. e.g. { [username: string] : IPerson; }Blandishment
Do we need the semicolon after IPerson in the first line? It seems that I can compile without any error without that semicolon.Tamarau
@DamnVegetables It can be omitted as the interface in this case only lists one property. When multiple properties are defined, they need to be delimited. Prior to v1.6 this had to be wuth a semicolon. Since v1.6 you can also choose to use a comma. I prefer using the semicolon as this is also the end-of-line delimiter, but the choice is purely personal. See this SO post for more background: #27994753Disclamation
C
171

For using dictionary object in typescript you can use interface as below:

interface Dictionary<T> {
    [Key: string]: T;
}

and, use this for your class property type.

export class SearchParameters {
    SearchFor: Dictionary<string> = {};
}

to use and initialize this class,

getUsers(): Observable<any> {
        var searchParams = new SearchParameters();
        searchParams.SearchFor['userId'] = '1';
        searchParams.SearchFor['userName'] = 'xyz';

        return this.http.post(searchParams, 'users/search')
            .map(res => {
                return res;
            })
            .catch(this.handleError.bind(this));
    }
Comnenus answered 19/7, 2017 at 10:44 Comment(0)
L
68

I agree with thomaux that the initialization type checking error is a TypeScript bug. However, I still wanted to find a way to declare and initialize a Dictionary in a single statement with correct type checking. This implementation is longer, however it adds additional functionality such as a containsKey(key: string) and remove(key: string) method. I suspect that this could be simplified once generics are available in the 0.9 release.

First we declare the base Dictionary class and Interface. The interface is required for the indexer because classes cannot implement them.

interface IDictionary {
    add(key: string, value: any): void;
    remove(key: string): void;
    containsKey(key: string): bool;
    keys(): string[];
    values(): any[];
}

class Dictionary {

    _keys: string[] = new string[];
    _values: any[] = new any[];

    constructor(init: { key: string; value: any; }[]) {

        for (var x = 0; x < init.length; x++) {
            this[init[x].key] = init[x].value;
            this._keys.push(init[x].key);
            this._values.push(init[x].value);
        }
    }

    add(key: string, value: any) {
        this[key] = value;
        this._keys.push(key);
        this._values.push(value);
    }

    remove(key: string) {
        var index = this._keys.indexOf(key, 0);
        this._keys.splice(index, 1);
        this._values.splice(index, 1);

        delete this[key];
    }

    keys(): string[] {
        return this._keys;
    }

    values(): any[] {
        return this._values;
    }

    containsKey(key: string) {
        if (typeof this[key] === "undefined") {
            return false;
        }

        return true;
    }

    toLookup(): IDictionary {
        return this;
    }
}

Now we declare the Person specific type and Dictionary/Dictionary interface. In the PersonDictionary note how we override values() and toLookup() to return the correct types.

interface IPerson {
    firstName: string;
    lastName: string;
}

interface IPersonDictionary extends IDictionary {
    [index: string]: IPerson;
    values(): IPerson[];
}

class PersonDictionary extends Dictionary {
    constructor(init: { key: string; value: IPerson; }[]) {
        super(init);
    }

    values(): IPerson[]{
        return this._values;
    }

    toLookup(): IPersonDictionary {
        return this;
    }
}

And here is a simple initialization and usage example:

var persons = new PersonDictionary([
    { key: "p1", value: { firstName: "F1", lastName: "L2" } },
    { key: "p2", value: { firstName: "F2", lastName: "L2" } },
    { key: "p3", value: { firstName: "F3", lastName: "L3" } }
]).toLookup();


alert(persons["p1"].firstName + " " + persons["p1"].lastName);
// alert: F1 L2

persons.remove("p2");

if (!persons.containsKey("p2")) {
    alert("Key no longer exists");
    // alert: Key no longer exists
}

alert(persons.keys().join(", "));
// alert: p1, p3
Lucilius answered 8/4, 2013 at 16:14 Comment(7)
Very helpful sample code. The "interface IDictionary" contains a small typo, as there is a reference to IPerson.Nedi
would be nice to implement element count as wellSpeight
@Lucilius The declaration containsKey(key: string): bool; does not work with TypeScript 1.5.0-beta. It should be changed to containsKey(key: string): boolean;.Untraveled
why dont you delcare generic type? Dictionary<T>, then no need to create the PersonDictionary class. You declare it like this : var persons = new Dictionary<IPerson>();Etching
I have used such a generic dictionary effectively. I found it here:fabiolandoni.ch/…Dominican
wont this cause problems if someone adds the key names one of the dictionary method names 'Add'Botulism
should class Dictionary not be class Dictionary implements IDictionary instead? I think you forgot it, right?Exhaust
S
32

Typescript fails in your case because it expects all the fields to be present. Use Record and Partial utility types to solve it.

Record<string, Partial<IPerson>>

interface IPerson {
   firstName: string;
   lastName: string;
}

var persons: Record<string, Partial<IPerson>> = {
   "p1": { firstName: "F1", lastName: "L1" },
   "p2": { firstName: "F2" }
};

Explanation.

  1. Record type creates a dictionary/hashmap.
  2. Partial type says some of the fields may be missing.

Alternate.

If you wish to make last name optional you can append a ? Typescript will know that it's optional.

lastName?: string;

https://www.typescriptlang.org/docs/handbook/utility-types.html

Snuffle answered 30/6, 2020 at 5:22 Comment(0)
A
13

Record<Tkey, Tobject> kinda works like a C# dictionary

let myRecord: Record<string, number> = {}; 

//Add
myRecord[”key1”] = 1;

//Remove
delete myRecord[”key1"];

//Loop
for (var key in myRecord) {
    var value = myRecord[key];
}
Aggiornamento answered 21/7, 2022 at 14:29 Comment(0)
U
11

Here is a more general Dictionary implementation inspired by this from @dmck

    interface IDictionary<T> {
      add(key: string, value: T): void;
      remove(key: string): void;
      containsKey(key: string): boolean;
      keys(): string[];
      values(): T[];
    }

    class Dictionary<T> implements IDictionary<T> {

      _keys: string[] = [];
      _values: T[] = [];

      constructor(init?: { key: string; value: T; }[]) {
        if (init) {
          for (var x = 0; x < init.length; x++) {
            this[init[x].key] = init[x].value;
            this._keys.push(init[x].key);
            this._values.push(init[x].value);
          }
        }
      }

      add(key: string, value: T) {
        this[key] = value;
        this._keys.push(key);
        this._values.push(value);
      }

      remove(key: string) {
        var index = this._keys.indexOf(key, 0);
        this._keys.splice(index, 1);
        this._values.splice(index, 1);

        delete this[key];
      }

      keys(): string[] {
        return this._keys;
      }

      values(): T[] {
        return this._values;
      }

      containsKey(key: string) {
        if (typeof this[key] === "undefined") {
          return false;
        }

        return true;
      }

      toLookup(): IDictionary<T> {
        return this;
      }
    }
Uncap answered 6/1, 2019 at 13:8 Comment(0)
T
9

If you are looking for an easy way to create a dictionary even in typescript, it is to use Map object. The link to the documentation https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map . Map object has the main methods of adding, retrieving and deleting and removing all elements.

dictionary= new Map<string, string>();
dictionary.set("key", "value");
dictionary.get("key");
dictionary.delete("key");
dictionary.clear(); //Removes all key-value pairs
Trainload answered 4/7, 2022 at 10:28 Comment(0)
P
5

If you want to ignore a property, mark it as optional by adding a question mark:

interface IPerson {
    firstName: string;
    lastName?: string;
}
Pharisaic answered 9/4, 2014 at 8:55 Comment(1)
The whole point of the question is why the given code compiled without providing a last name…Spokeshave

© 2022 - 2024 — McMap. All rights reserved.