Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

string or number dictionary type parameter

Tags:

typescript

I have this function:

interface NumDict<T> {
  [key : number] : T
}

export function mapNumDictValues<T,R>(dict: NumDict<T>, f: (v: T, key?: number) => R): NumDict<R> {
  let emptyDict : NumDict<R> = {};
  return Object.keys(dict).reduce((acc, key) => {
    const keyInt = parseInt(key);
    acc[keyInt] = f(dict[keyInt], keyInt);
    return acc;
  }, emptyDict);
}

Now I would like it to work for string indexed dictionaries as well as number indexed dictionaries, e.g. something like:

function mapDictValues<K extends string|number,T,R>(obj: {[id: K]: T}, f: (v: T, key?: K) => R): {[id: K]: R} {

However, this gets me this error:

error TS1023: An index signature parameter type must be 'string' or 'number'.

Is there a way?

like image 604
Lodewijk Bogaards Avatar asked Jun 09 '16 09:06

Lodewijk Bogaards


2 Answers

Try this:

interface IStringToNumberDictionary {
    [index: string]: number;
}


interface INumberToStringDictionary {
    [index: number]: string;
}

type IDictionary = IStringToNumberDictionary | INumberToStringDictionary;

Example:

let dict: IDictionary = Object.assign({ 0: 'first' }, { 'first': 0 });
let numberValue = dict["first"]; // 0
let stringValue = dict[0]; // first

In your case something like this:

interface IStringKeyDictionary<T> {
    [index: string]: T;
}


interface INumberKeyDictionary<T> {
    [index: number]: T;
}

type IDictionary<T> = IStringKeyDictionary<T> | INumberKeyDictionary<T>;
like image 180
V L Avatar answered Oct 21 '22 20:10

V L


What you are after is not easily accomplished (at least I can't find a way) because of how javascript treats the object keys (strings only) and typescript restrictions on index expressions (string/number/symbol/any) and the difference between the types number | string and number/string.

The only type-safety you managed to gain (from what I understand) is that you get an error using noImplicitAny (obscure error) for one case, but you are still restricted with what you can do with this dictionary.

I don't know much about your case, but it sounds like you have little to gain here in order to get decent solution, unless you really need this type-safety then I think it's better to just treat your keys as strings and get it over with, but if you do need it then I suggest that you create your own dictionary type implementations to deal with things, for example:

interface Dict<K extends number | string, V> {
    get(key: K): V;
    set(key: K, value: V): void;
    mapValues<R>(f: (v: V, key?: K) => R): Dict<K, R>;
}

abstract class BaseDict<K extends number | string, V> implements Dict<K, V> {
    protected items: { [key: string]: V } = Object.create(null);

    get(key: K): V {
        return this.items[key.toString()];
    }

    set(key: K, value: V): void {
        this.items[key.toString()] = value;
    }

    abstract keys(): K[];

    values(): V[] {
        return Object.keys(this.items).map(key => this.items[key]);
        // or Object.values(this.items) with ES6
    }

    mapValues<R>(fn: (v: V, key?: K) => R): Dict<K, R> {
        let dict: Dict<K, R> = Object.create(this.constructor.prototype);
        this.keys().forEach(key => dict.set(key, fn(this.get(key), key)));
        return dict;
    }
}

class NumDict<V> extends BaseDict<number, V> {
    keys(): number[] {
        return Object.keys(this.items).map(key => parseFloat(key));
    }
}

class StringDict<V> extends BaseDict<string, V> {
    keys(): string[] {
        return Object.keys(this.items);
    }
}

You'll need to create your dicts with the ctors, which is not as comfortable as using {}, but you do have more control over things, for example notice that when calling the map function (fn) in BaseDict.mapValues then the key will have the right type (number in case of NumDict and string for StringDict).

like image 20
Nitzan Tomer Avatar answered Oct 21 '22 19:10

Nitzan Tomer