Open IanKemp opened 5 years ago
why don't
T extends StandardSortOrder | AlternativeSortOrder
why don't
T extends StandardSortOrder | AlternativeSortOrder
Because I want to allow IThingThatUsesASortOrder
to be used with any enum type.
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 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
}
@dragomirtitian Exactly correct (also, thanks for your answer on Stack Overflow).
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)
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)
function test<T extends string | number>(): {
} { return null; }
i don't now why it's work!
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)) {
....
}
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])));
}
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]
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:
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.
@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.
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?
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
.
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!
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 <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.
<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;
}
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:
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.
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:
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;
So I considered index signatures too. If I try index signatures, I get a double whammy of TS 2322 and 2344!
enum FeedbackMessageTheme {
Error = 'error',
Info = 'info',
}
type Literal<T extends { [key: string]: any }> = `${T}`;
type FeedbackMessageThemeLiteral = Literal<FeedbackMessageTheme>;
const theme: FeedbackMessageThemeLiteral = FeedbackMessageTheme.Info;
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
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
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",
};
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.
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: :eyes:
and in switch case
@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)
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.
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.
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:
<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.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?
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 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
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>;
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;
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.
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.
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:
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).
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>,
};
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!
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>'.
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.
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
@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"
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.
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.