microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.08k stars 12.5k forks source link

Proposal: String enums #3192

Closed jbondc closed 7 years ago

jbondc commented 9 years ago

A string enum is inferred by looking at the constant value of the initializer of its first member:

enum stringsInferred {
   a = "a",
   b, // "b"
   c  // "c"
}

Or it is declared using a string index:

enum stringsDeclared {
   [prop: string] : string;
   a, // "a"
   B, // "B"
   c  // "c"
}

The auto initialized value on an enum is its property name.

Inferred enum special case

enum stringsLowerCase {
   Shoes = "shoes", // If the first member has lowercase chars, then lowercase all other member values
   Boots, // "boots"
   Feet  // "feet"
}
mariusschulz commented 9 years ago

:+1:

We should definitely be able to use enums with string values. Even a const enum with compile-time string literals (that are always safe to inline) would be helpful already, mainly for …

I'm not sure the automagical upper- and lowercasing described underneath Inferred enum special cases is such a good idea, though. I'd rather spell all cases exactly like they should be stringified:

enum stringsUpperCase {
   Shoes = "SHOES",  // "SHOES"
   BOOTS,            // "BOOTS"
   Feet              // "Feet"
}
dead-claudia commented 9 years ago

Can this be generalized to other types (with the constraint that all types must be initialized - no value can be inferred)?

dead-claudia commented 9 years ago

@jbondc

I agree, and this could be the other special case (where value inference already exists for numbers). Also, FWIW, I made a post on that bug with a few more specific suggestions IIRC. It just wasn't on the top of my mind at the time.

dead-claudia commented 9 years ago

@jbondc

And I feel this syntax is a little repetitive in the [prop: string]: string part. Couldn't the syntax be modified to have the type after the enum name, instead of in the body? Maybe this syntax, to borrow your example?

enum strings : string {
   Shoes,           // "Shoes"
   BOOTS,            // "BOOTS"
   Feet              // "Feet"
}
dead-claudia commented 9 years ago

@jbondc Any advantages to that over my recent proposal? I don't think that regex-based solution is really that helpful in practice. All that regex does there is enforce type names, something better left to a linter as opposed to the main compiler. And it looks a little out of place compared to the enum entries. And as you said in the other thread, it will complicate the parser and compiler a little, likely more than it should.

And as for extending enums, that's not very good practice, anyways. And it's generally not possible among other TypeScript users. Mine allows for such extensions, but you can only truly extend them in the same module or private namespace declaration, unless you're writing a definition file. That's because of the module wrapper emitted around every namespace declaration. So, for most purposes, you can't really extend them in the first place. Also, the spec currently requires that same-named declarations within the same root to add properties to the same enum.

lazdmx commented 9 years ago

:+1: for string (const) enums

rotemdan commented 9 years ago

My current workaround looks something like this (this is a real-world, copy-and-paste code example):

class Task {
  ..
  static State = { 
    Unstarted: "Unstarted", 
    Started: "Started", 
    Completed: "Completed", 
    Faulted: "Faulted", 
    Canceled: "Canceled"
  }
}

let task = new Task();
..
if (task.state === Task.State.Completed) // The declared type of task.state is "string" here.
  console.log("Done!");

This also gives a nice encapsulation into the class type that I find appealing (through the static modifier).

Replicating this through enum would require several different enhancements. I will consider posting a separate proposal for class encapsulated enums if that hasn't been proposed before.

I think having string enums would be very useful (maybe "necessary" should be the right word here). Automatic inferring of things like letters would be problematic because of locale and language issues (every language would have its own letters and ordering). Automatic lowercasing/uppercasing would also be impacted from similar issues (every language has different lower/uppercasing rules), and seems to me like a minor detail/enhancement at this point.

mariusschulz commented 9 years ago

Maybe the string keyword would lend itself to be included in the declaration:

string enum PromiseState {
    Pending,
    Fulfilled,
    Rejected
}

In the above example, the string values of the cases would equal their names. They could be overwritten like this:

string enum PromiseState {
    Pending = "pending",
    Fulfilled = "fulfilled",
    Rejected = "rejected"
}
rotemdan commented 9 years ago

This may turn out a bit long for a const enum that is also exported:

export const string enum PromiseState {
    Pending,
    Fulfilled,
    Rejected
}

So maybe an alternative using some sort of a psuedo-generic type parameter?

export const enum PromiseState<string> {
    Pending,
    Fulfilled,
    Rejected
}

Due to issues with different languages, alphabets, and locales it would be difficult to apply a correct conversion to lowercase/uppercase in many languages other than English. So just preserving the original casing and allowing custom ones through = seems like a reasonable compromise.

Gambero81 commented 9 years ago

Hi, what is the actual state of this proposal? can we have a roadmap to know when will be implemented in the language? Thanks

dead-claudia commented 9 years ago

@Gambero81 See #1206 for a more up-to-date version of this.

dead-claudia commented 9 years ago

Can this be closed in favor of #1206? It's more general, which is better IMHO.

dead-claudia commented 9 years ago

Union and intersection types would already exist with my proposal. You could achieve the same thing.

On Sun, Sep 20, 2015, 09:17 Jon notifications@github.com wrote:

@impinball https://github.com/impinball No, added this here strictly for discussing a string enum & use cases. e.g. can you interest two string enums?

enum a { hello = "hello" } enum b { world = "world" } let c = a & b; let d = typeof a & typeof b;

— Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/3192#issuecomment-141784947 .

vitrilo commented 9 years ago

Using of String are really neat feature. In our 20k loc project we still use workarounds. Apart, using objects as enum values (like in #1206 or http://stackoverflow.com/a/15491832/1224258) are risky - comparison fails after any Enums serialization/de-serialization or deepcopy (angular.copy).

basarat commented 9 years ago

Probably better if https://github.com/Microsoft/TypeScript/issues/1003 is done instead

dead-claudia commented 9 years ago

@basarat I think both have their uses. A primitive enum (excluding symbols) could be seen as the union of their values in that sense. And also, I believe that should be expanded to also include numbers.

dead-claudia commented 9 years ago

I made a relevant response over there on #1003.

dead-claudia commented 9 years ago

@jbondc I tried to include those, but I didn't get a very positive response from it.

vitrilo commented 9 years ago

@basarat Thanks, I've just discovered related #1003, #1295. Great proposals/ideas! My understanding #1003 (string literal union type) vs #3192 (string enums) is following:

    var var1 = MyEnum.Kind1; //string enum.
    var var2 = "kind1"; //string literal union type - NO compiler type-checking! 
    var var3:MyUnionEnum = "kind1"; //string literal union type with compiler type-checking 

Apart, from all above, Javascript ecosystem generally encourage wide usage of string literals. As result, like in #1003, #1295 IDEs/Tools will be forced to support special meaning of string literals, like as enum constants/properties names etc, not just meaningless text to output.

jtheoof commented 8 years ago

:+1: This feature would be great.

Right now, the enums are a bit counter intuitive from a JavaScript usage perspective. They work well for internal TypeScript code but not so much when code is used in third party apps.

It would be especially useful for bridging the gap between both worlds:

Example:

sdk.ts
--------

export enum Environments {
  SANDBOX,
  PRODUCTION
}

interface ClientOptions {
  id: string;
  secret: string;
  env?: Environments;
}

export class Client {
  constructor(public options: ClientOptions) {}

  hostname(): string {
    let part = 'sandbox';

    if (this.env === Environments.PRODUCTION) {
      part = 'api';
    }

    return `rest.${part}.example.com`;
  }
}

caller.js
----------

var client = new Client({
  id: 'id',
  secret: 'secret',
  env: 'production'
});

Right now, client.hostname() will fail because Environments.PRODUCTION === 1.

mrcrowl commented 8 years ago

:+1:

olmobrutall commented 8 years ago

My two cents while this gets approved:

Not that you can pretend this feature like this.

    export enum OrderType {
        Ascending = "Ascending" as any,
        Descending = "Descending" as any,
    }

The benefit is that then your API is better documented, asking for an OrderType instead of a dirty string.

The problem is that you need to repeat the identifier for the common case when declaring.

Also, when using it, you have to cast from string -> any -> OrdeType:

var orderType: OrderType = "Ascending"; //Error
var orderType: OrderType = "Ascending" as any; //OK
DanielRosenwasser commented 8 years ago

Edit: Maybe don't do this since string literal types are still in flux.


Here's a way you'll be able to emulate string enums in typescript@next. Taking a modified version of Esprima's Syntax object:

function strEnumify<T extends { [prop: string]: "" | string }>(obj: T) {
    return obj;
}

export const Syntax = strEnumify({
    AssignmentExpression: 'AssignmentExpression',
    AssignmentPattern: 'AssignmentPattern',
    ArrayExpression: 'ArrayExpression',
    ArrayPattern: 'ArrayPattern',
    ArrowFunctionExpression: 'ArrowFunctionExpression',
    BlockStatement: 'BlockStatement',
    BinaryExpression: 'BinaryExpression',
    BreakStatement: 'BreakStatement',
    CallExpression: 'CallExpression',
    CatchClause: 'CatchClause',
    ClassBody: 'ClassBody',
    ClassDeclaration: 'ClassDeclaration',
    ClassExpression: 'ClassExpression',
    // ...
    ThisExpression: 'ThisExpression',
    ThrowStatement: 'ThrowStatement',
    TryStatement: 'TryStatement',
    UnaryExpression: 'UnaryExpression',
    UpdateExpression: 'UpdateExpression',
    VariableDeclaration: 'VariableDeclaration',
    VariableDeclarator: 'VariableDeclarator',
    WhileStatement: 'WhileStatement',
    WithStatement: 'WithStatement',
    YieldExpression: 'YieldExpression'
});

image

(@ariya)

The problem is that you can't do something like the following:

let kind = Syntax.VariableDeclaration;
kind = Syntax.WithStatement;
vitrilo commented 8 years ago

@olmobrutall thanks for perfect workaround! Well serialized/deserialized, apart from unreadable numeric enums inside json. Below working even in v1.4:

  enum OrderType { Ascending = <any>"Ascending", Descending = <any>"Descending" }
dead-claudia commented 8 years ago

@DanielRosenwasser I would generally prefer to use const numeric enums where possible (a JS-to-ESTree AST parser couldn't for spec reasons) for that just for memory and performance reasons (both reduced for free). But I could see merit in a string enum to hold error messages like what jshint uses.

Gambero81 commented 8 years ago

@olmobrutall @DanielRosenwasser @vitrilo there workaround are good but a good feature of string enum should be to inlining string const enum without emit js as done with standard const enum, unfortunally for this porpouse all these workaround doesn't work

olmobrutall commented 8 years ago

@Gambero81, of course having native support from the language will be the best solution, simplifying the declaration (no repetition for the simple cases), and the usage (no casting).

About const enums, we already have the syntax and the concept for normal enums, it will make sense just to extrapolate the same concepts for string enums.

export const enum Color extends string{
   red,
   blue,
   darkViolet= "dark-violet"
}
hgoebl commented 8 years ago

:+1: current project: accessing REST service with generated TypeScript interfaces and enums. Enums are not usable, since service needs strings in JSON payload.

Is there a way to serialize / deserialize JSON and convert enum instances on-the-fly?

olmobrutall commented 8 years ago

I've just found this today: https://blog.wearewizards.io/flow-and-typescript-part-2-typescript

TLDR; Typescript is better than flow but has no string enums :-1:

The only issues we have encountered so far (and would love help to resolve!) are:

  • webpack building the previous version: adding an empty line and saving a file re-triggers a compilation but can be frustrating
  • no string enums: we want to keep human readable enum members but TypeScript only supports int enums

Any news on this issue? Any change this gets implemented.

Gambero81 commented 8 years ago

I agree.. string enum is a MUST!! (in particular const string enum)

still hope to show it soon...

sampsyo commented 8 years ago

I just read that the new TypeScript 1.8 beta has "string literal types": https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#string-literal-types

That seems to solve exactly this use case, right?

type Order = "Ascending" | "Descending";
Gambero81 commented 8 years ago

string literal is not string enum..

olmobrutall commented 8 years ago

Wow didn't know that. With autocompletion they can be a even better alrernative!

column.order = //autocompletion here please!
               //"Ascending"
               //"Descending"

Thanks TS Team!

basarat commented 8 years ago

@olmobrutall I have two issues created for your tracking :rose: https://github.com/Microsoft/TypeScript/issues/5602 https://github.com/Microsoft/TypeScript/issues/6705

dsherret commented 8 years ago

Edit: See better way here.

String enums can be mimicked by merging a string literal type and module this way:

type MyStringEnum = "member1" | "member2";

module MyStringEnum {
    export const Member1: MyStringEnum = "member1";
    export const Member2: MyStringEnum = "member2";
}

Full example.

If at all, it seems it's only worth implementing enum MyStringEnum { myMember = "my member" } for syntactic sugar.

olmobrutall commented 8 years ago

@dsherret nice point!

dead-claudia commented 8 years ago

@dsherret s/module/namespace/g. module in that context is deprecated, and it has been for a while. :smile:

dsherret commented 8 years ago

This works just like a string enum too:

type MyStringEnum = "member1" | "member2";

const MyStringEnum = {
    Member1: "member1" as MyStringEnum,
    Member2: "member2" as MyStringEnum
};

Updated example.

amcdnl commented 8 years ago

Clever hack @dsherret but why is this not implemented yet? +1 for string enums!

dead-claudia commented 8 years ago

@amcdnl I feel the more general #1206 is more helpful than just this.

ixrock commented 8 years ago

I came up with such solution:

// Utils.ts
export function convertEnumValuesToString(obj) {
  Object.keys(obj).forEach(function (key) {
    if (isNaN(+key)) {
      Object.defineProperty(obj, key, {
        value: key,
        enumerable: true
      });
    }
  });
  return obj;
}

// actions/hello.ts
import { convertEnumValuesToString } from '../utils'

export enum actionTypes {
  GREETING,
  FAREWELL_INIT,
  FAREWELL_DONE
}

console.log(actionTypes .GREETING) // 0
convertEnumValuesToString(actionTypes); // override to same key-based string values
console.log(actionTypes .GREETING) // "GREETING"

Maybe would be helpful to someone :)

theRemix commented 8 years ago

+1 would love to have this for angular2 ngrx reducer types.

i've been using this as string enums for now

export class Color {
  public static red = "red";
  public static blue = "blue";
  public static green = "green";
}
amcdnl commented 8 years ago

@theRemix check out this decorator I wrote - https://medium.com/@amcdnl/enums-in-es7-804a5a01bd70#.z0emdxdy1

zspitz commented 8 years ago

@dsherret Your hack doesn't work for ambient declarations.

dsherret commented 8 years ago

@zspitz just need to change the syntax accordingly:

type MyStringEnum = "member1" | "member2";
declare const MyStringEnum: { Member1: MyStringEnum; Member2: MyStringEnum; };
zspitz commented 8 years ago

@dsherret This still won't inline the values (unlike const enum), although I'm not sure any of the workarounds on this page will allow this, or if there is any language feature that supports inlining values other than number via enums.

OliverJAsh commented 8 years ago

TS 2 allows you to refer to enums as types as well as values, e.g.

enum ColorTypes { Red, Green, Blue }
type RedColor = { type: ColorTypes.Red }
const redColor: RedColor = { type: ColorTypes.Red }

As far as I can see, none of the workarounds above will allow for this behaviour?

cwmoo740 commented 8 years ago

Using typescript@2.0.2 this seems to work for me:

namespace Colors {
    export type Red = 'Red';
    export type Blue = 'Blue';
    export const Red:Red = 'Red';
    export const Blue:Blue = 'Blue';
}
type Colors = Colors.Red | Colors.Blue;

It's ugly but then you can refer to it as a type and a string enum.

const myColors:{[key:string]: Colors, thirdThing: Colors.Red, fourthThing: colors.Blue} = 
{
    something: Colors.Red, 
    somethingElse: Colors.Blue, 
    thirdThing:'Red',
    fourthThing: Colors.Blue
};
RyanCavanaugh commented 8 years ago

I think we can dupe this to #1206 since the only other type really being discussed there is string and there's some progress being made to some solution

malcompm commented 8 years ago

Hi TS Team, Seems like this is the most upvoted issue in this repository - so could we please at least get some update if this even get considered/done ? Seems like it does not even exist on the roadmap for Future https://github.com/Microsoft/TypeScript/wiki/Roadmap :(