Typescript: Allow "T extends enum" generic constraint

Created on 27 Mar 2019  路  14Comments  路  Source: microsoft/TypeScript

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;
}
Awaiting More Feedback Suggestion

Most helpful comment

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)

All 14 comments

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(): {
} {
return null;
}

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

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:

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

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

CyrusNajmabadi picture CyrusNajmabadi  路  3Comments

dlaberge picture dlaberge  路  3Comments

jbondc picture jbondc  路  3Comments

blendsdk picture blendsdk  路  3Comments

manekinekko picture manekinekko  路  3Comments