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

Allow "T extends enum" generic constraint #30611

Open
IanKemp opened this issue Mar 27, 2019 · 38 comments
Open

Allow "T extends enum" generic constraint #30611

IanKemp opened this issue Mar 27, 2019 · 38 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@IanKemp
Copy link

IanKemp commented Mar 27, 2019

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 via T extends string | number which neither conveys the intent of the programmer, nor imposes the requisite type enforcement.

export enum StandardSortOrder {
    Default,
    Most,
    Least
}

export enum AlternativeSortOrder {
    Default,
    High,
    Medium,
    Low
}

export interface IThingThatUsesASortOrder<T extends enum> { // doesn't compile
    sortOrder: T;
}
@Kingwl
Copy link
Contributor

Kingwl commented Mar 27, 2019

why don't

T extends StandardSortOrder | AlternativeSortOrder

@IanKemp
Copy link
Author

IanKemp commented Mar 27, 2019

why don't

T extends StandardSortOrder | AlternativeSortOrder

Because I want to allow IThingThatUsesASortOrder to be used with any enum type.

@jcalz
Copy link
Contributor

jcalz commented Mar 27, 2019

Duplicate of #24293?

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 2, 2019
@RyanCavanaugh
Copy link
Member

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

@dragomirtitian
Copy link
Contributor

@RyanCavanaugh Not to quibble but I think the intent is for sortOrder to be an enum member. So sortOrder should probably be T[keyof T]

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 StandardEnum name, so a type StandardEnumValue = string | number would achieve just as much and be more succint IMO:

type StandardEnumValue = string | number
export interface IThingThatUsesASortOrder<T extends StandardEnumValue> { 
    sortOrder: T;
}

type K2 = IThingThatUsesASortOrder<AlternativeSortOrder>
let s2: K2 = {
    sortOrder: AlternativeSortOrder.Default
}

@IanKemp
Copy link
Author

IanKemp commented Apr 4, 2019

@dragomirtitian Exactly correct (also, thanks for your answer on Stack Overflow).

@lrdxg1
Copy link

lrdxg1 commented Jun 13, 2019

Doesn't seem to work in TS 3.5.1 :(

Type 'typeof AlternativeSortOrder' does not satisfy the constraint 'StandardEnum<unknown>'.
  Index signature is missing in type 'typeof AlternativeSortOrder'.ts(2344)

@seanlaff
Copy link

seanlaff commented Nov 12, 2019

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)

@anchmelev
Copy link

function test<T extends string | number>(): {
[key in T]: string;
} {
return null;
}

i don't now why it's work!
image

@Semigradsky
Copy link

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)) {
	....
}

@KEMBL
Copy link

KEMBL commented May 8, 2020

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

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:

static EnumToArray<T extends string, TEnumValue extends number | string>(enumVariable: {[key in T]: TEnumValue}): string[] {
    return Object.keys(enumVariable).filter((key) => !isNaN(Number(enumVariable[key])));
}

@ethaizone
Copy link

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.

enum Region { sg, th, us }
enum Env { dev, stg, prod }

// It works but I don't want to create 10 functions for 10 Enum.
const fetchAsRegion = (val: string): Region => Region[val as keyof typeof Region]
const fetchAsEnv = (val: string): Env => Env[val as keyof typeof Env]

// I hope this function can work.
const fetchAsEnum = <T extends enum>(val: string): T => T[val as keyof typeof T]

@nicky1038
Copy link

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

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:

  • this check happens at runtime;
  • the type of enumVariable , { [key in T]: TEnumValue }, is a type of the JavaScript object, to which the enum is being transpiled; it seems like a leaky abstraction;
  • Object.values(enumVariable) creates an extra array, to iterate over enumVariable efficiently we need to write even more code, as enums do not provide any special iteration functionality.

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.

@papermana
Copy link

papermana commented Aug 19, 2020

@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 MyType extends number | string. You can also extend number or string only, if you consistently use numeric or string enums.

@athyuttamre
Copy link

athyuttamre commented Jan 4, 2021

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?

@psxvoid
Copy link

psxvoid commented Jan 4, 2021

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

@DarrenDanielDay
Copy link

I also have a related problem for enum.

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 T extends enum generic constraint, the implementation code would be like this:

type Describe<T> = {
  [K in keyof T]: T extends enum ? "enum" : T extends number ? "number" : never
}

The C# language has enum constraint, and I hope TypeScript can also have it!

@sinaa
Copy link

sinaa commented Jun 2, 2021

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

@sh-pq
Copy link

sh-pq commented Jul 23, 2021

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 <app-feedback-message theme="info">. If I want to allow that, I have to define a template literal type. See (a) and (b) in this snippet:

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 theme="info" or [theme]="FeedbackMessageTheme.Info both work.

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 <T extends Enum>

If there were <T extends Enum> constraint I could skip defining the type entirely. I could just define an enum literal pattern once and use it in loads of places:

// defined in a neighbouring folder
export type Literal<T extends Enum> = `${T}`;
import { Literal } from '../utils/literal';

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: Literal<FeedbackMessageTheme> = FeedbackMessageTheme.Info;
}

Confirmation this could work (probably?)

You can confirm this would work in theory by examining this snippet:

enum FeedbackMessageTheme {
  Error = 'error',
  Info = 'info',
}

type Literal<T extends FeedbackMessageTheme> = `${T}`;

type FeedbackMessageThemeLiteral = Literal<FeedbackMessageTheme>;

const theme: FeedbackMessageThemeLiteral = FeedbackMessageTheme.Info;

Here's a screenshot of that snippet inside Visual Studio Code:

a working example of the above code snippet, with autocomplete showing the editor is satisfied with it

This doesn't appear replicable using currently available tools

I tried a few things to try to get some poor man's version of Literal<T extends Enum> and couldn't find a way to implement it.

Perhaps there's some solution available somehow, but I'm already jumping through hoops as-is. This is a relatively advanced use of the language, but even then doing this shouldn't require performing Olympic-grade acrobatics.

Mapped types can't replicate this

My first thought was to try using a mapped type to try to implement a poor man's version of this, but TypeScript doesn't like that and throws TS 2322:

ts 2322 on line 6 of the below snippet

enum FeedbackMessageTheme {
  Error = 'error',
  Info = 'info',
}

type Literal<T extends { [key in keyof T]: any }> = `${T}`;

type FeedbackMessageThemeLiteral = Literal<FeedbackMessageTheme>;

const theme: FeedbackMessageThemeLiteral = FeedbackMessageTheme.Info;

Index signatures can't replicate this

So I considered index signatures too. If I try index signatures, I get a double whammy of TS 2322 and 2344!

ts 2322 on line 6 of the below snippet
ts 2344 on line 8 of the below snippet

enum FeedbackMessageTheme {
  Error = 'error',
  Info = 'info',
}

type Literal<T extends { [key: string]: any }> = `${T}`;

type FeedbackMessageThemeLiteral = Literal<FeedbackMessageTheme>;

const theme: FeedbackMessageThemeLiteral = FeedbackMessageTheme.Info;

@edwardfoyle
Copy link

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 T extends string evaluates to true when T is an enum type. For example:

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 T extends enum existed, the above testFunc could be defined as

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

@totszwai
Copy link

Hi, what happened to this?

@mixaildudin
Copy link

Would be nice to have this feature since C# has language-level support for this...

@william9-99
Copy link

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

@DarrenDanielDay
Copy link

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 Record<YourEnum, string>?

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

@skafendre
Copy link

Would appreciate such a feature, the current proposed workaround doesn't capture as clearly the expectation of an enum

@Distortedlogic
Copy link

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.

@smaznet
Copy link

smaznet commented Oct 28, 2022

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

and it works: 👀
image

and in switch case
image

@Uraharadono
Copy link

Uraharadono commented Apr 2, 2023

@smaznet I have found your solution to be quite nice, but is not providing proper results at least for me.

I have following enum:

export enum EPostVisibility {
	Public = 0,
	Private = 1,
	Unlisted = 2,
}

( 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 :
(4) [0, 1, 2, 'Public']

Problem is in the filter +1 : .filter((e, index, array) => index < array.length / 2 + 1)
If you just remove + 1, it should be fine: .filter((e, index, array) => index < array.length / 2)

@TonyGravagno
Copy link

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 case

An 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
  • Types PersonFields1, PersonFields2, and PersonFields3 are three type definitions that provide the same service for this purpose.
  • PersonFields1 relies on the ability to derive a union of keys from an object. It can be used where types already exist and enums only duplicate the names. That is, derive the fieldnames from somewhere else rather than getting type safety in that somewhere else from a redundant enum.
  • PersonFields2 is a simple union of text, equivalent for this purpose to an enum.
  • PersonFields3 is a set of keys derived from PersonFieldsEnum3, an actual enum example that we cannot pass as generic . If you already have enums, use this one-liner.
  • In const PersonText, change the generic type from PersonFields1,2,3 and note that the result is the same for all of them. Choose your favorite solution. The types are all typesafe, IDE hints and compile-time checking are fully functional.
  • In my application I import {PersonFieldsZ as Fields} and use in-line to avoid having to change the definition in-line if I actually need a different type style.

Playground to see it working

@TonyGravagno
Copy link

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:

  • The service/function-side signature <T=never,U extends T=T> is funky but not unheard of. This enforces the requirement for any generic constraint and can be used with any type, not just enums.
  • The client-side function call unfortunately requires redundant syntax.

Explanation:

We can't just pass the enum value like Person.name because that's a simple numeric, and without context for the function there's nothing for compile-time checking to test for validity. If we just pass a typeof Person, the function still only has an index and a type, and the compiler can't tell if they are related - which is why we want T extends enum in the first place. Even with the type of the enum identified, it can't be used in the function because enums are objects, not types. To use the actual enum, it needs to be passed to the function if we want to dynamically translate a numeric index back into a property/enum name. If you don't need to operate on the enum, you don't need the object in the params.

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 above
enum 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

@owl-from-hogvarts
Copy link

owl-from-hogvarts commented Jul 6, 2023

One more use case here is generic enum map. I can't think of way to do it without extends enum:

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

@quangquyen1410
Copy link

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]

image

@Benjamin-Dobell
Copy link

Benjamin-Dobell commented Sep 2, 2023

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

EnumCompatibleValue would be Literal in your example.

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;

CleanShot 2023-09-02 at 16 42 33

Playground Tests

ValueToEnum can be used to enforce (at compile time) that mappings of some sort exist between literals and corresponding enum values — this is my use case.

@paolosimone
Copy link

paolosimone commented Oct 30, 2023

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:

enum_select

@brianbraunstein
Copy link

brianbraunstein commented Nov 6, 2023

I think I have a more satisfying and widely applicable solution.

Requirement

I 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:

  • use standard enums (not forced into classes/etc).
  • use enums stored efficiently as numbers (not forced into string).
  • make use of the already available enum string representation mapping (not forced into a separate mapping).

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 StateMachine implementation below to show this.

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

Solution

Here'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 Usage

Here'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 Example

Now 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...):

from=INIT to=IM_GOOD
from=IM_GOOD to=HUNGRY
from=HUNGRY to=EATING
from=EATING to=IM_GOOD
Error: Invalid transition: from=IM_GOOD to=POISONED
    at StateMachine.transitionTo (<anonymous>:20:13)
    at <anonymous>:50:4
    at mn (<anonymous>:16:5455)

Doing this however:

enum DoesntHaveInit {FOO, BAR};
const sm2 = new StateMachine(DoesntHaveInit, {});  // error as expected.

Results in the fairly satisfying error message of:

Argument of type 'typeof DoesntHaveInit' is not assignable to parameter of type 'EnumObject<typeof DoesntHaveInit> & EnumWithInit<typeof DoesntHaveInit>'.
  Property 'INIT' is missing in type 'typeof DoesntHaveInit' but required in type 'EnumWithInit<typeof DoesntHaveInit>'.

Disclaimer

I'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.

@litera
Copy link

litera commented Jul 2, 2024

I've now been playing with this T extends enum, and gotten to this type definition that (as far as I've written it) supports enums with number base type.

type IsTsEnum<T> = [T] extends [number] ? ([number] extends [T] ? T : never) : never;

Then I've also written a utility type that returns the enum actual values:

type EnumValues<TEnum extends string | number> = `${TEnum}` extends `${infer T extends number}`
  ? T
  : `${TEnum}` extends `${infer T extends string}`
    ? T
    : never;

And an example:

enum Color {
  Red,
  Green,
  Blue
}

type EColor = IsTsEnum<Color>; // Color
type ColorValues = EnumValues<Color>; // 0 | 1 | 2

@DoK6n
Copy link

DoK6n commented Aug 26, 2024

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

@G0x209C
Copy link

G0x209C commented Nov 14, 2024

Yeah.. I just resorted to using

export type IsEnumType = Record<string, string|number>

But you can still add a record at that point, which isn't completely bad but not exactly right..
Types in typescript are more like semantics anyway, so just keep in mind that when you see a type requirement of IsEnumType you use something that is an enum. You can potentially abuse this, but that misuse is up to you.

Note that you are required to use typeof in your generic function calls when passing in an enum.
Like this:

// In my service:
public async openSelectionDialog<TEnum extends IsEnumType, TResult>(someDataObjectThatContainsTEnum: {options: TEnum}): Promise<TResult | DialogActionResultType>
// In my actual component
const result = await dialogService.openSelectionDialog<typeof myEnum, myEnum>({options: myEnum});

This way I can pass in my enum as both the entire object and the requirements for the type of the result, but it also lets me expect some more generic dialog interaction results like "OK", "Yes", "Cancel", etc.
Then I set up a switch to go over the results and make my decision based on that.
If cancel, do nothing, if of TResult, do something with the selection, if of unsupported response type throw RangeError.

I'm still feeling the disappointed of when I found out that I can't just do something like:
T extends (typeof) enum
Like you can with number, string, array, etc. But it's not a JS type.. Maybe we should open a proposition for that, eh?
But egh, the web is made of "make-dos" and a lot of duct-tape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests