Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Index signature parameter type should allow for enums #2491

Closed
jhlange opened this issue Mar 25, 2015 · 32 comments
Closed

Index signature parameter type should allow for enums #2491

jhlange opened this issue Mar 25, 2015 · 32 comments
Labels
Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript

Comments

@jhlange
Copy link

jhlange commented Mar 25, 2015

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,
}
@bgever
Copy link

bgever commented May 21, 2015

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

@jbondc
Copy link
Contributor

jbondc commented May 31, 2015

+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;
}

@jbondc
Copy link
Contributor

jbondc commented May 31, 2015

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
}

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Jul 14, 2015
@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed In Discussion Not yet reached consensus labels Aug 5, 2015
@RyanCavanaugh
Copy link
Member

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)

@jbondc
Copy link
Contributor

jbondc commented Aug 28, 2015

(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.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 28, 2015
@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision Too Complex An issue which adding support for may be too complex for the value it adds and removed In Discussion Not yet reached consensus labels Oct 5, 2015
@RyanCavanaugh
Copy link
Member

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

@jbondc
Copy link
Contributor

jbondc commented Oct 7, 2015

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

@RyanCavanaugh
Copy link
Member

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.

@bgever
Copy link

bgever commented Oct 8, 2015

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

@jbondc
Copy link
Contributor

jbondc commented Oct 8, 2015

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

@jbondc
Copy link
Contributor

jbondc commented Oct 8, 2015

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.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 14, 2016

Should be possible now with MappedTypes.

So the example in the OP can be:

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

@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed Declined The issue was declined as something which matches the TypeScript vision Revisit An issue worth coming back to Too Complex An issue which adding support for may be too complex for the value it adds labels Nov 14, 2016
@mhegazy mhegazy added this to the TypeScript 2.1.3 milestone Nov 14, 2016
@jkillian
Copy link

@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"]

@icholy
Copy link

icholy commented Dec 14, 2016

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

@ChiriVulpes
Copy link

ChiriVulpes commented Dec 19, 2016

@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)

@mhegazy
Copy link
Contributor

mhegazy commented Dec 19, 2016

This was broken by #12425. would you mind logging a new issue to allow numeric literals as constraints of mapped types?

@mattiLeBlanc
Copy link

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?

@apetrushin
Copy link

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

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

@Xample
Copy link

Xample commented Apr 27, 2017

@apetrushin you can use "in" for this

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

@apetrushin
Copy link

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

@mattiLeBlanc
Copy link

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?

@nevir
Copy link

nevir commented Jun 29, 2017

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

@aselbie
Copy link

aselbie commented Aug 2, 2017

It appears this is a known regression with a fix on the roadmap: #13042

@yordis
Copy link

yordis commented Dec 8, 2017

@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?

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Nov 5, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Projects
None yet