316
votes

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.

6
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; }'. - Simon_Weaver
Can you please update this post: the title doesn't align the with question and the accepted answer! - minus one
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. - tkonsta

6 Answers

360
votes

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


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
119
votes

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));
    }
66
votes

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
15
votes

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

9
votes

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

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

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