Typescript: Index signature parameter type should allow for enums

Created on 25 Mar 2015  ·  32Comments  ·  Source: microsoft/TypeScript

Typescript requires that enums have number value types (hopefully soon, this will also include string value types).

Attempting to use an enum as a key type for a hash results in this error: "Index signature parameter type much be 'string' or 'number' ".-- An enum is actually a number type.-- This shouldn't be an error.

Enums are a convenient way of defining the domain of number and string value types, in cases such as

export interface UserInterfaceColors {
    [index: UserInterfaceElement]: ColorInfo;
}
export interface ColorInfo {
    r: number;
    g: number;
    b: number;
    a: number;
}
export enum UserInterfaceElement {
    ActiveTitleBar = 0,
    InactiveTitleBar = 1,
}
Fix Available Suggestion

Most helpful comment

Should be possible now with MappedTypes.

So the example in the OP can be:

export type UserInterfaceColors = {
    [P in UserInterfaceElement]: ColorInfo;
}

All 32 comments

+1, this is a great way for type-safe dictionaries.

+1

If the TS team embraces 'set types' #3105, this would work too:

interface SomeArrayLikeThing {
    [index: 0...255]: number;
}

enum foo {
 [prop: string]: 0...255
 a,
 b,
 c
}

interface SomeArrayLikeThingByEnum {
    [index: foo]: number;
}

Would likely require at the minimum in the compiler something like:

export interface ObjectIndex {
       valueType: Type        // any
       keyType?: Type         // string|number or any subset type
}
export interface ResolvedType extends ObjectType, UnionType {
       members: SymbolTable;              // Properties by name
       properties: Symbol[];              // Properties
       callSignatures: Signature[];       // Call signatures of type
       constructSignatures: Signature[];  // Construct signatures of type
       stringIndex?: ObjectIndex;            // String index
       numberIndex?: ObjectIndex;        // Numeric index
}

This does seem very useful. Going back to the original example, we had some questions about the intended semantics. Given some types:

enum Color { red, green, blue }
enum Size { small, medium, large }
interface Alpha {
  [c: Color]: string;
}
interface Beta {
  [c: Size]: string;
}
interface Gamma {
  [c: number]: string;
}
interface Delta {
  [c: Color]: string;
  [c: Size]: string;
}

Obvious things:

  • You can't index an Alpha by a Size

Non-obvious things:

  • Is Alpha assignable to Beta ?
  • Is Gamma assignable to Alpha ? Vice versa?
  • Can I index an Alpha by a number ?
  • Is Delta a legal declaration? (strongly leaning toward _no_)

(a)

Is Alpha assignable to Beta ?
Is Delta a legal declaration?

No

(b)

Can I index an Alpha by a number ?
Is Gamma assignable to Alpha ? Vice versa?

Yes (unfortunately), follow isTypeAssignableTo()

const enum Color { red, green, blue }
let a: number;
let b: Color;
a = 4;
b = 4; // unfortunately (c)

interface Alpha {
  [c: Color]: string;
}
let c: Alpha;
c[Color.red];
c[0];
c[4]; // unfortunately (c)

I'd be happy if (c) changed eventually so that const enums only accept constants or types within their range of values.

While the use cases make sense, we didn't think this provided enough value to justify its added complexity.

Would be great to elaborate on Meh (Meeting Exchange History)

There's not a huge amount to add -- we understand the use cases, we understand the value proposition, and we understand the additional complexity. Given a finite complexity 'budget', we didn't think this was worthwhile compared to other things we could do instead.

So it's a resource constraint rather than technical complexity?

@RyanCavanaugh Don't get it, #1863 #2012 are still open so there's a type indexer budget for those?

More elaborate example:

type Foo = {};
interface IndexTypes {
  [oneTo5: 1...5]: string;
  [sixTo20: 6...20]: number;
  [someSymbol]: Foo;
}
let a: IndexTypes;
a[0]; // string
a[6]; // number
a[someSymbol]; // Foo

Supporting symbols means your complexity budget goes up anyways.

Now the compiler has something like:

export interface ResolvedType extends ObjectType, UnionType {
       members: SymbolTable;              // Properties by name
       properties: Symbol[];              // Properties
       callSignatures: Signature[];       // Call signatures of type
       constructSignatures: Signature[];  // Construct signatures of type
       stringIndex?: ObjectIndex;            // String index
       numberIndex?: ObjectIndex;        // Numeric index
       otherIndexes?: ObjectIndex[]
}

Just don't see what other things you could do.

We all understand that there's a clear use case for this, shouldn't this go into the backlog instead of being closed?

+1 interested in any other patterns for achieving the TypeSafety of only being able to index an object using a specific const enum or (as jbondc pointed out) unfortunately a number too.

+1 dictionaries are very useful but dangerous as is with the restriction of indexing only by a high-level type. This forces them to be loosely typed. I've worked around this with explicit casting of enum to number with the "+" operator and exposing a type safe public interface but would prefer this was a function of the language.

Here is a contrived example:

enum Domestic {
    Cat,
    Dog
}

enum Wild {
    Bear,
    Tiger
}

class AnimalController<T> {

    private _lists: { [key:T]: IAnimal[] };

    // constructor...

    public register( type:T, animal:IAnimal ) {
        this._lists[ +type ].push( animal );
    }

    public getListByType( type:T ): IAnimal[] {
        return this._lists[ +type ]; // << cast enum to number
    }
}

var dac = new AnimalController<Domestic>();

// register animals ...

dac.getListByType( Domestic.Cat ); // OK

// correctly fails with "not assignable" error.
dac.getListByType( Wild.Bear );

if instead getListByType() accepted any number. E.G:

public getListByType( type:number ): IAnimal[] {
    return this._lists[ type ];
}

// This would not be caught at compile time and since
// Domestic.Cat and Wild.Bear are the same value 
// it would return a valid ( but very wrong ) list...
dac.getListByType( Wild.Bear );

Please consider backloging this rather than closing.

Please consider to include this feature.. how about relating an string with an enum, is a very common practice to associate any kind of data with an enum.

If people could "react" to the OP instead of posting +1s, that would be useful both in terms of reducing the noise here, and in helping us get an accurate count of feedback. Thanks!

(+1s deleted, seriously, use reactions)

I shamelessly react to all posts on this thread

Should be possible now with MappedTypes.

So the example in the OP can be:

export type UserInterfaceColors = {
    [P in UserInterfaceElement]: ColorInfo;
}

@mhegazy: it seems like it might be a little trickier than that in practice?

export enum MyEnum {
    First = 0,
    Second = 1,
}

export type EnumMap = {
    [P in MyEnum]: string;
}

// [ts] Type '{ [x: number]: string; }' is not assignable to type 'EnumMap'.
//      Property '0' is missing in type '{ [x: number]: string; }'.
const doesntWork: EnumMap = {
    [MyEnum.First]: "first",
    [MyEnum.Second]: "second",
};

const works: EnumMap = {
    0: "first",
    1: "second",
};

// but then this is allowed, should it be?
works["foo"] = "bar";
// but then: 
// [ts] Property 'foo' does not exist on type 'EnumMap'.
type test = typeof works["foo"]

You can use any type as the key for Map and if you really want type safe object keys, you can wrap it.

class DictValues<V> {
    [key: string]: V;
    [key: number]: V;
}

interface ToStringer {
  toString(): string;
}

class Dict<K extends ToStringer, V> {
  private _values: DictValues<V> = Object.create(null);

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

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

  has(key: K): boolean {
    return this._values.hasOwnProperty(key.toString());
  }

  values(): DictValues<V> {
    return this._values;
  }
}

@mhegazy: I get Type 'UserInterfaceColors' is not assignable to type 'string' when using the code that should work:

export type UserInterfaceColors = {
    [P in UserInterfaceElement]: ColorInfo;
}

Coupled with the bits @JKillian mentioned makes me think this should be reopened (or at least the Fixed tag removed)

This was broken by https://github.com/Microsoft/TypeScript/pull/12425. would you mind logging a new issue to allow numeric literals as constraints of mapped types?

Hi, I would also benefit from being able to use a declared type as index, so that I can restrict which kind of indexes are being used.

For example:

declare type validationTypes = 'email' | 'number' | 'abn';

interface IField {
  label:                string;
  model:                string;
  type:                 fieldType;
  placeholder?:         string;
  addon?:               string;
}

interface IEmailValidation {
  email:              string;
}
interface IRequiredValidation {
  required:             string;
}


interface IEmailField extends IField {
  type:                'email';
  validation:        IDictionary<IEmailValidation | IRequiredValidation>
}

What I try to achieve is that the validation will be a dictionary and has the key 'email' and the value type of string, and a required validation.

A text field would only have Required validation.

The Javascript object looks like this:

const emailField =        {
          "label":                "Email Address",
          "model":               "emailaddress",
          "type":                 "email",
          "placeholder":          "Please enter an email address",
          "validation": {
            "required":          "An email address is required",
            "email":              "Please enter a valid email address"
          }
        }

Right now I can only allow all possible validations but I can't restrict the set of validations per field type. This is because of the index must to be string or number restriction.

If it would be possible to define a dictionary like this:

export interface IDictionary<K,V> {
  [index: key]: V;
}
then K could be a declare type of 'email' | 'required' for an email field and only 'required' for a text field.

Any thoughts?

This seems to be quite frequent pattern, would be nice to support it.

type keys = 'a' | 'b' | 'c'
let map: { [key: keys]: string } = {}

@apetrushin you can use "in" for this

type keys = 'a' | 'b';
let anObject: { 
    [index in keys]: string;
}
anObject.a = "correct";
anObject.t = "error";

Thanks, but it's still allows to do anObject['non-existing-key'] = 'value'

Have a look at my post just above. That would be an awesome addition. Right now we can only defined the index of a dictionary to be of a string but not restrict to a list of string types.
Any change this can become a feature?

This appears broken again as of TS 2.4 (2.x?)

enum Things { ONE, TWO }
// Error: Type 'Things' is not assignable to type 'string'. 
type ThingMap = {[TKey in Things]: boolean};

It appears this is a known regression with a fix on the roadmap: https://github.com/Microsoft/TypeScript/issues/13042

@nevir I am using 2.6 and it is working fine for me but because I match the enum keys to some string

enum Things { ONE = 'one', TWO = 'two' }

Should be that the default behavior? or should be just doing the enum without any mapping?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

weswigham picture weswigham  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments