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

Better support for enumerating properties in generics using string enums #18409

Closed
trevorade opened this issue Sep 12, 2017 · 2 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@trevorade
Copy link

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)

Code

Example:

/// An examination of string enums when used to enumerate values and properties.

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

enum AltVals {
    UNO = 'one',
}

/// First, a look at how string enums are treated as the value of a property in
/// a property in a generic interface.

// Note: Note: In this case, `extends string` is unnecessary.
interface LimitedVals<TVals extends string> {
    readonly val: TVals;
}

// The errors in limited1 and limited3 make sense.
const limited1: LimitedVals<Vals> = { val: 'one' };                     // Error
const limited2: LimitedVals<Vals> = { val: Vals.ONE };
const limited3: LimitedVals<Vals> = { val: AltVals.UNO };               // Error

/// Next, a look at how string enums are treated as possible properties in a
/// generic interface.

// Perhaps `extends string` isn't the most appropriate for string enums.
// Presumably, it's just defining the properties as strings.
interface LimitedProps<TProps extends string> {
  readonly values: {[prop in TProps]: number};
}

// It may be desirable for the same errors as found in limited1 above to occur
// for limProps0 and limProps1.
const limProps0: LimitedProps<Vals> =
    { values: { one: 1, two: 2 } };
const limProps1: LimitedProps<Vals> =
    { values: { 'one': 1, 'two': 2 } };
// Computed properties should work and should probably be the preferred syntax.
const limProps2: LimitedProps<Vals> =                                   // Error
    { values: { [Vals.ONE]: 1, [Vals.TWO]: 2 } };
// Should still result in an error but not because of the computed property.
// AltVals.UNO is from the wrong enum even though it has the same string value.
const limProps3: LimitedProps<Vals> =                                   // Error
    { values: { [AltVals.UNO]: 1, [Vals.TWO]: 2 } };

// Again, these two property reference styles are inconsistent with the error in
// limited1.
limProps1.values.one;
limProps1.values['one'];
// Arguably, this should be the preferred syntax.
limProps1.values[Vals.ONE];
// This shouldn't work as it's the wrong enum type.
limProps1.values[AltVals.UNO];

Expected behavior:
A generic interface should be able to take a string enum as a template type to define properties. When using an instance of this type generic interface, property type checking should occur. See the example above for suggestions of what should and should not work.

It's possible that problem is the usage of extends string for the generic type in the interface. I'm guessing that the compiler loses the fact that the properties should be the type of the string enum and instead just sees string properties. If that's the case, perhaps we need a better way to tell the type checker that we expect a string enum here. Like <TProps extends enum<string>> or something.

Actual behavior:
When creating a property that is an instance of a generic interface using a string enum as type, the "enumness" of the type is lost. It's just strings at this point. See the above example for current behavior.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 12, 2017

For the first issue, string enum elements are tagged versions of the string literal type matching their value. In other words, Vals.One is a special version of the literal type "one". Vals.One is a subtype of "one" but not the other way around. Similarly AltVals.Uno is another subtype of "one", but it is not the same as Vals.One. as a result:

declare let Vals_ONE: Vals.ONE;
declare let AltVals_Uno: AltVals.UNO;
declare let One: "one";

One = Vals_ONE; // OK
One = AltVals_Uno;  // OK

Vals_ONE = One; // Error
Vals_ONE = AltVals_Uno; // Error

for the second. this has been addressed in #18317 and should be working as expected in typescript@next today.

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 12, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Oct 3, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Oct 3, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants