-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Allow "T extends enum" generic constraint #30611
Comments
why don't T extends StandardSortOrder | AlternativeSortOrder |
Because I want to allow |
Duplicate of #24293? |
This is effectively what you want, though it will allow "enum-like" things (which is probably a feature) type StandardEnum<T> = {
[id: string]: T | string;
[nu: number]: string;
}
export interface IThingThatUsesASortOrder<T extends StandardEnum<unknown>> {
sortOrder: T;
}
type K = IThingThatUsesASortOrder<typeof AlternativeSortOrder>; |
@RyanCavanaugh Not to quibble but I think the intent is for type StandardEnum<T> = {
[id: string]: T | string;
[nu: number]: string;
}
export interface IThingThatUsesASortOrder<T extends StandardEnum<unknown>> {
sortOrder: T[keyof T];
}
type K = IThingThatUsesASortOrder<typeof AlternativeSortOrder>;
export enum AlternativeSortOrder { Default, High, Medium, Low }
let s: K = {
sortOrder: AlternativeSortOrder.Default
} Also this approach will work with a wide range of types not just enums, which was the original request. const values = { a: "A" } as const
type K2 = IThingThatUsesASortOrder<typeof values>;
let s2: K2 = {
sortOrder: "A"
}
type A = { a: string }
type K3 = IThingThatUsesASortOrder<A>; So this does not really enforce the must be enum constraint, and only expresses the intent of having an enum through the type StandardEnumValue = string | number
export interface IThingThatUsesASortOrder<T extends StandardEnumValue> {
sortOrder: T;
}
type K2 = IThingThatUsesASortOrder<AlternativeSortOrder>
let s2: K2 = {
sortOrder: AlternativeSortOrder.Default
} |
@dragomirtitian Exactly correct (also, thanks for your answer on Stack Overflow). |
Doesn't seem to work in TS 3.5.1 :(
|
I have a use case for this as well. I have a Select component that I want to make generic by taking any enum. I expect the caller to pass in a map of enumValue -> string labels. If this feature existed, I could make a component like function EnumSelect<T extends Enum>(options: { [e in T]: string }) {
...
} Which would guarantee type-safety. (The compiler would prevent the developer ever forgetting to map a certain enum value to a user-friendly string representation) |
It works beautiful: function createEnumChecker<T extends string, TEnumValue extends string>(enumVariable: { [key in T]: TEnumValue }) {
const enumValues = Object.values(enumVariable)
return (value: string): value is TEnumValue => enumValues.includes(value)
}
enum RangeMode {
PERIOD = 'period',
CUSTOM = 'custom',
}
const isRangeMode = createEnumChecker(RangeMode)
const x: string = 'some string'
if (isRangeMode(x)) {
....
} |
Thanks! With a small modification works fine in Typescript 2.8.0 for enums with number | string variable types. It Takes enum and returns a string array of value names:
|
I want to share another case for this too. It's simple case for convert string to be enum. It will use when I read environment variable as string and I want output to be enum.
|
It's a usable workaround, we also use it as there is no better solution for TypeScript enums. But there are still some huge drawbacks:
Once again, these drawbacks exist not because the workaround is bad, but because TypeScript doesn't allow to solve the problem in better way. It remains to hope that somewhen TypeScript enums will be much more powerful and get rid of these disadvantages. |
@Andry361's solution does work. Thank's Andry! For those confused as to why it works, here's an explanation (as I understand it). Enums are implemented as objects, but on a type level they represent a union of their values, not an object containing those values. enum Foo {
a = 'a',
}
interface Bar {
a: 'a',
}
const foo: Foo = 'a'; // valid because type Foo represents any one of that enum's values
const bar: Bar = 'a'; // invalid because a value of type Bar has to be an object implementing that interface Or in other words enum Foo {
a = 'a',
b = 'b',
}
type Test = Foo extends 'a' | 'b' ? 'true' : 'false'; // 'true' Enums have two possible types for their values: numbers or strings. So the most generic way to see if a type is an enum is to do |
I also have a use case for this. I want to create a general purpose state machine abstraction that takes a user defined enum as the state. type StateMachine<State extends enum> = {
value: State;
transition(from: State, to: State): void;
}; Anyone have any tips on if this is possible with TypeScript today? |
On my project, I was trying to find a similar solution to this problem. I ended up using classes instead of enums. Similarly to this, it would also make sense to specify a "base" (more narrowed) enum constraint. But it may require to allow enums to implement sort of a "type enum" (or "enum interface"). For example: type enum StateMachineEnum extends string;
type StateMachine<State extends StateMachineEnum> = {
value: State;
transitionTo(state: State): void;
};
enum AbstractState extends StateMachineEnum
{
State1 = "Abstract State 1",
State2 = "Abstract State 2"
}
enum NonAbstractState extends StateMachineEnum
{
State1 = "Non Abstract State 1",
State2 = "Non Abstract State 2"
}
let sm = new StateMachine<StateMachineEnum>();
sm.transitionTo(AbstractState.State1);
sm.transitionTo(NonAbstractState.State2); Having enum constraints and narrowed enum constrains may reduce errors when several enum types can be specified. Also, narrowed enum constraints may be a better option than using union-types because when you create another enum, that should satisfy that constraint, you only need to extend it from a specific "type enum" (modification in one place) instead of creating the enum and adding it to the union-type (two places to modify). |
I also have a related problem for enum Foo {
a = 1,
b = 2,
}
interface Bar {
enumProp: Foo;
numberProp: number;
}
// Note that Foo is an arbitrary custom enum type.
// TODO: Write a generic type Describe<T> to infer a description type, like this:
type Describe<T> = {
// TODO: implementation
}
type Baz = Describe<Foo>
/* Expected result
type Baz = {
enumProp: "enum";
numberProp: "number";
}
*/ If TypeScript has type Describe<T> = {
[K in keyof T]: T extends enum ? "enum" : T extends number ? "number" : never
} The |
On a related problem on ensuring enum keys and a corresponding type mapping, we came across a scenario where you would want to ensure the property of a given type exists based on a generic enum key. enum Keys {
foo = 'foo',
bar = 'bar',
buzz = 'buzz'
}
interface Types {
foo: number
bar: string
buzz: Symbol
} We solved this using: type WithProperty<K extends Keys> = { [T in K extends K ? K : never] : Types[K]}
const x: WithProperty<Keys.bar> // this will ensure x.bar is available, but excludes other types on `Types` This is particularly useful when you want to ensure the value in a generic way: function foo(input: WithProperty<Keys.foo>): number {
// input.foo is available, and is of type number
// input.bar is not available
return input.foo
} Effectively, with separate keys and value types which are matching, you can assert the type exists (typechecker is satisfied with the below): function bar<T extends Keys>(key: T, resources: WithProperty<T>): Types[T] {
return resources[key]
} |
In Angular, I use enums to constrain the values that can be provided to a component. A simplistic example is the following: public enum FeedbackMessageTheme {
Error = 'error',
Info = 'info',
}
@Component({ selector: 'app-feedback-message' })
export class FeedbackMessageComponent {
/** The theme for styling the feedback, determining the icon, etc */
@Input() theme: FeedbackMessageTheme = FeedbackMessageTheme.Info;
} However, this doesn't allow string input, so I can't write public enum FeedbackMessageTheme {
Error = 'error',
Info = 'info',
}
type FeedbackMessageThemeLiteral = `${FeedbackMessageTheme}`
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ (a)
@Component({ selector: 'app-feedback-message' })
export class FeedbackMessageComponent {
/** The theme for styling the feedback, determining the icon, etc */
@Input() theme: FeedbackMessageThemeLiteral = FeedbackMessageTheme.Info;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ (b)
} Now either However, I need to define this enum and the corresponding type every single time I do this pattern. Defining the literal in one place using
|
Another use case where this would be useful is differentiating between literal strings / numbers and enum values. If the input shape of a function is conditional on a generic type, this cannot always be expressed given the fact that type StringInput = {
a: string
b: string
};
type OtherInput = {
x: string
y: string
}
const testFunc = <T>(input: T extends string ? StringInput : OtherInput): T => {}
enum Color {
RED = '#FF0000',
YELLOW = '#FFFF00',
}
type Example = {}
testFunc<string>({a: 'foo', b: 'bar'}) // valid
testFunc<Example>({ x: 'hello', y: 'world' }) // valid
testFunc<Color>({ a: 'foo', b: 'bar' }) // valid
testFunc<Color>({ x: 'hello', y: 'world' }) // Argument of type ... is not assignable to type 'StringInput' I understand why this is the behavior, but there are cases where I want the Color enum type to behave like it is not a literal string. If const testFunc = <T>(input: T extends string ? (T extends enum ? OtherInput : StringInput) : OtherInput): T => {} With the expected behavior of: testFunc<Color>({ a: 'foo', b: 'bar' }) // Argument of type ... is not assignable to type 'OtherInput'
testFunc<Color>({ x: 'hello', y: 'world' }) // valid |
Hi, what happened to this? |
Would be nice to have this feature since C# has language-level support for this... |
Similar use case to what seanlaff but in our project we have an api that sends codes to the front end indicating what a user can and cannot do. We have it in an enum so we can easily add new options project wide. In the ui we need to convert those codes to human readable strings. Being able to enforce that all the members of this enum are mapped would be very useful for us |
Possibly enum Options {
Cancel,
Ok,
}
type OptionsText = Record<Options, string>;
// Use `OptionsText` to ensure every enum is mapped in the object:
const englishOptions: OptionsText = {
[Options.Cancel]: "cancel",
[Options.Ok]: "ok",
}; |
Would appreciate such a feature, the current proposed workaround doesn't capture as clearly the expectation of an enum |
I love Enums, this is one of the things python does better and def needs more attention on the TS side. Not only extending like in this issue, but also I should be able to put both instance and static methods on my Enum. Ik that methods dont fit with the current Enum implementation at all, but Enums do deserve better than what they are currently in all reality and seriousness. |
i wrote this for getting list of items in enum (like dart)type EnumValueType = string | number | symbol;
type EnumType = { [key in EnumValueType]: EnumValueType };
type ValueOf<T> = T[keyof T];
type EnumItems<T> = Array<ValueOf<T>>;
export function getEnumItems<T>(item: EnumType & T): EnumItems<T> {
return (
// get enum keys and values
Object.values(item)
// Half of enum items are keys and half are values so we need to filter by index
.filter((e, index, array) => index < ( array.length / 2 + 1 ))
// finally map items to enum
.map((e) => item[e as keyof T]) as EnumItems<T>
);
} |
@smaznet I have found your solution to be quite nice, but is not providing proper results at least for me. I have following enum:
( I have tested this by adding new items in this enum to make it even and odd) And using this code would yield following result : Problem is in the filter +1 : |
My skill in this area is far from the level of others here, but I do have a solution that seems to work so far. I don't know if it's a universal solution, but I have three ways of addressing the challenge that all work, so it's a solution for a number of scenarios, extremely easy to use, and I hope this will help someone here. Summary of one example (what everyone cares about)enum PersonFieldsEnum3 {
firstName,
lastName,
birthDate,
}
type PersonFields3 = keyof typeof PersonFieldsEnum3 // E Pluribus Unum
const PersonText: Translatable<PersonFields3> = {
Labels: {
firstName: 'First Name',
lastName: 'Last Name',
birthDate: 'Birth Date'
}
interface Translatable<T extends string> { // important extends string
Labels: { [key in T] : string }
} My use caseAn app has a Person class with fields defined as schema. The UI is dynamic, getting Labels and other text from a separate component. Translatable text components must have a Labels property with one value for every field defined in the schema. Change the schema and the UI begs to be modified with it. Ideally fieldnames are just enums, as values are dependent on context so no value is required. But we can't pass an enum as a generic type to enforce compliance across components. Detailed explanation
Playground to see it working |
Follow-up : I am encountering scenarios where I need more robust handling than handled by my prior suggestion. This is better, until there's official support. It's not pretty, but hacks often are not, and this one isn't too bad. enum Approved { yes, no }
function enumAsGeneric<T=never,U extends T=T>(which: number, obj: T): void { }
const test = enumAsGeneric<Approval,Approval>(Approved.yes,Approved) This requires two changes to the standard contract:
Explanation: We can't just pass the enum value like Note: The request in this ticket is for "T extends enum". Yes, that's great, but unless we can use an instance of the enum of type T, the functionality is limited. So I hope instantiate is a part of this if it's ever implemented. I tried to manage this through a literal type template but was unsuccessful. Anyone else want to see if that's doable? More complete example from aboveenum Approved { yes, no }
type Approval = typeof Approved
function enumAsGeneric<T=never,U extends T=T>(which: number, obj: T): string {
const array = Object.keys(obj as string[]).filter(key => isNaN(Number(key)))
return `${Object.keys(obj as string[])[which]} = ${array[which]}`
}
const test = enumAsGeneric<Approval,Approval>(Approved.yes,Approved)
console.log( test ) Playground with extensive tests showing compile-time errors |
One more use case here is generic enum map. I can't think of way to do it without type EnumMap<T extends enum> = {
[Key in T]: string
} Such type is required to map enum's constants to actual values dynamically In fact, enum map is possible. But for every enum you want to map there is a need to create appropriate enum map type. Generics would remove excess code here |
I wrote the code below to get enum values with a generic type: export const getEnumValues = <T extends object>(item: T): Array<T[keyof T]> => {
return Object.values(item).filter((_, index, array) => index > array.length / 2 - 1);
};
enum TestEnum {
x,
y,
z
}
const values = getEnumValues(TestEnum);
console.log(`Log => values:`, values);
// values is [0, 1, 2] |
@sh-pq I think the following will do what you were after: type StandardEnum = Record<string, string | number>
type StringLikeValues<T> = { [K in keyof T]: T[K] extends string ? T[K] : never }[keyof T]
type StringValues<T> = StringLikeValues<T> extends string ? `${StringLikeValues<T>}` : never
type EnumCompatibleValue<Enum extends StandardEnum> = Enum[keyof Enum] | StringValues<Enum>;
Can also extend this to obtain an enum from a literal with: type ValueToEnum<Enum extends StandardEnum, N extends EnumCompatibleValue<Enum>> =
N extends infer Member extends Enum[keyof Enum]
? Member
: N extends `${infer Member extends Enum[keyof Enum]}` ? Member : never;
|
I recently ended up here searching for the same use case "EnumSelect React Component" raised by @seanlaff Probably it has already been solved countless times, nevertheless I'll leave here my solution (based on @smaznet modeling) hoping it may be helpful to future readers. type EnumType = { [key: string]: number | string };
type ValueOf<T> = T[keyof T];
type EnumSelectProps<T extends EnumType> = {
enumType: T;
labels: Record<ValueOf<T>, string>;
};
function EnumSelect<T extends EnumType>(props: EnumSelectProps<T>): JSX.Element {
return (
<select>
{Object.values(props.enumType).map((value) => (
<option key={value} value={value}>
{props.labels[value as ValueOf<T>]}
</option>
))}
</select>
);
} Intellisense/compiler will complain if a label is missing: |
I think I have a more satisfying and widely applicable solution. RequirementI wanted a solution that allows for what the following is conceptually trying to do, and what you might expect to work coming from a C++ background: class Bundle<TEnum extends enum> {
list: Array[TEnum];
add(item: TEnum) { list.push(item); }
printAll() {
for (const i of list) {
console.log(TEnum[i]);
}
}
}
enum Fruits { APPLE, ORANGE }
enum Colors { RED, BLUE }
const b = new Bundle<Colors>();
b.add(Color.RED);
b.add(Color.RED);
b.add(Color.BLUE);
b.printAll(); // Prints RED RED BLUE
b.add(Fruits.APPLE) // should error. In particular, the solution should allow the generic function to:
This will work for the state machine use case mentioned above by @psxvoid @athyuttamre, which is also what brought me here. I provide an example The solutions provided by @dragomirtitian and @RyanCavanaugh solved the original posters specific problem, but I think the original poster's question was more general and these solutions weren't general (which is also fine, but I think a lot of people end up on this bug who are looking for a general solution). SolutionHere's the key part of the solution: type EnumValue<TEnum> = TEnum[keyof TEnum] & number|string;
type EnumObject<TEnum> = {
[k: number]: string,
[k: string]: EnumValue<TEnum>,
}; Simple Example UsageHere's how to implement the above conceptual sketch for real: class Bundle<TEnum extends EnumObject<TEnum>> {
enumObj: TEnum;
list: Array<EnumValue<TEnum>> = [];
constructor(enumObj: TEnum) {
this.enumObj = enumObj;
}
add(item: EnumValue<TEnum>) { this.list.push(item); }
printAll() {
for (const i of this.list) {
console.log(this.enumObj[i]);
}
}
}
enum Fruits { APPLE, ORANGE }
enum Colors { RED, BLUE }
const b = new Bundle(Colors);
b.add(Colors.RED);
b.add(Colors.RED);
b.add(Colors.BLUE);
b.printAll(); // Prints RED RED BLUE
b.add(Fruits.APPLE) // error as expected! State Machine ExampleNow here's an example state machine implementation. It also demonstrates how to enforce that specific values are in the enum like "INIT": type EnumWithInit<TEnum> = {INIT: EnumValue<TEnum>};
type TransitionConfig<TEnum> = {
[Property in EnumValue<TEnum>]: Array<EnumValue<TEnum>>;
};
class StateMachine<TEnum extends EnumObject<TEnum>
& EnumWithInit<TEnum>> {
enumObj: TEnum;
state: EnumValue<TEnum>;
validTransitions: TransitionConfig<TEnum>;
constructor(enumObj: TEnum,
validTransitions: TransitionConfig<TEnum>) {
this.enumObj = enumObj;
this.state = this.enumObj.INIT;
this.validTransitions = validTransitions;
}
transitionTo(toState: EnumValue<TEnum>) {
const transitionLog = `from=${this.enumObj[this.state]} to=${this.enumObj[toState]}`;
if (toState != this.state &&
!this.validTransitions[this.state].includes(toState)) {
throw new Error(`Invalid transition: ${transitionLog}`);
}
console.log(transitionLog);
this.state = toState;
}
} And the usage of it: enum States { INIT, IM_GOOD, HUNGRY, EATING, POISONED, HOSPITAL, DEAD }
const sm = new StateMachine(States,
{
[States.INIT]: [States.IM_GOOD, States.DEAD],
[States.IM_GOOD]: [States.HUNGRY],
[States.HUNGRY]: [States.EATING, States.DEAD],
[States.EATING]: [States.POISONED, States.IM_GOOD],
[States.POISONED]: [States.HOSPITAL],
[States.HOSPITAL]: [States.IM_GOOD, States.DEAD],
[States.DEAD]: [],
}
);
sm.transitionTo(States.IM_GOOD);
sm.transitionTo(States.HUNGRY);
sm.transitionTo(States.EATING);
sm.transitionTo(States.IM_GOOD);
sm.transitionTo(States.POISONED); // will throw, can't be poised without eating.
// (in this model at least). This prints (and yes, I'm currently hungry...):
Doing this however: enum DoesntHaveInit {FOO, BAR};
const sm2 = new StateMachine(DoesntHaveInit, {}); // error as expected. Results in the fairly satisfying error message of:
DisclaimerI'm just getting started with typescript so don't consider this an expert answer. Suggestions/corrections are welcome. If it's a good solution it would be useful if an actual expert could endorse it so that others know whether to second guess it or not. |
Partially closer, still can't quite figure out how to properly associate both the `name` field and the `id` field inside of the `RegistryEntry` type. https://minecraft.wiki/w/Smithing_Template#Armor_trim_patterns https://stackoverflow.com/questions/72455619/passing-generic-enum-type-as-a-parameter-to-a-function-in-typescript https://stackoverflow.com/questions/70905278/how-to-force-two-discriminated-unions-to-have-the-same-type-for-their-discrimina microsoft/TypeScript#30611 https://stackoverflow.com/questions/43747234/typescript-is-it-possible-to-create-a-mapped-union-type
I've now been playing with this
Then I've also written a utility type that returns the enum actual values:
And an example:
|
@litera I've modified it to support both string and number and Mixed. It seems like it could be very useful! type StringToNumber<T extends string | number> = T extends `${infer N extends number}` ? N : never
type EnumValues<TEnum extends string | number> = `${TEnum}` extends `${infer T extends
number}`
? T
: `${TEnum}` extends `${infer T extends string}`
? T extends `${number}`
? StringToNumber<T>
: T
: never
enum StringColor {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
enum MixedColor {
Red = 'Red',
Green = 123,
Blue = 'Blue',
}
enum NumberColor {
Red,
Green,
Blue,
}
type StringColorValues = EnumValues<StringColor> // "Red" | "Green" | "Blue"
type NumberColorValues = EnumValues<NumberColor> // 0 | 1 | 2
type MixedColorValues = EnumValues<MixedColor> // "Red" | 123 | "Blue" |
TypeScript has a discrete
enum
type that allows various compile-time checks and constraints to be enforced when using such types. It would be extremely useful to allow generic constraints to be limited to enum types - currently the only way to do this is viaT extends string | number
which neither conveys the intent of the programmer, nor imposes the requisite type enforcement.The text was updated successfully, but these errors were encountered: