Excluding enums but still returning enums in TypeScript

Viewed 262

In order to use a subset of items in a typescript enum, we do something like this:

enum Operator {
  EQUAL = 'EQUAL',
  NOT_EQUAL = 'NOT_EQUAL',
  LESS = 'LESS',
  GREATER = 'GREATER',
}

type EqualOperators = Exclude<Operator, Operator.LESS | Operator.GREATER>

Is there any way where I can get EqualOperators to be an enum instead of a type, this way I could use it in a script?

To clarify, when I say "use it in a script", I mean being able to use the enum values in a javascript function, which you can't to with types, e.g. something like this:

enum Operator {
  EQUAL = 'EQUAL',
  NOT_EQUAL = 'NOT_EQUAL',
  LESS = 'LESS',
  GREATER = 'GREATER',
}

function getOperator() {
  if (Operator.EQUAL) {
    return "=";
  }
  if (Operator.NOT_EQUAL) {
    return "!=";
  }

  throw new Error('invalid operator');
}

Thanks!

3 Answers

As was mentioned, typescript enums function both as types and objects. To be clear, all types and interfaces disappear at runtime, but objects remain. So, in other words, to have a condition like if (op === Operator.EQUAL), Operator must exist at runtime (and it does, because we're using the "object-half" of the enum here).

In your example, the Operators "object" will act like this object:

{
  EQUAL: 'EQUAL',         // Operator.EQUAL
  NOT_EQUAL: 'NOT_EQUAL', // Operator.NOT_EQUAL
  LESS: 'LESS',           // Operator.LESS
  GREATER: 'GREATER',     // Operator.GREATER
}

Now, when you use TypeScript's type system to derive a new type from the enum, all you did was create a type, but to use if as an object (like in the if condition), you need to also instantiate an object to go with it. I came up with the following, which has the benefit of leveraging the type system to ensure that you don't leave anything out when you create your constant object (and also ensures that only the appropriate value gets assigned to each "key").

type EqualOperators = Omit<typeof Operator, 'LESS' | 'GREATER'>;

const EqualOperators: EqualOperators = {
  EQUAL: Operator.EQUAL,
  NOT_EQUAL: Operator.NOT_EQUAL,
}

I defined EqualOperators to be as close to your usage as I could. I'm not sure if some requirement constrains you to this approach, but if not, my personal recommendation is to instead use the Pick utility type. i.e:

type EqualOperators = Pick<typeof Operator, 'EQUAL' | 'NOT_EQUAL'>;

The advantage is that you are clearly stating what the "equality" operators are, and if you add new operators, you only need to remember to update the EqualOperators type when they are equality operators, which I feel is a more natural thought process than remembering to update the EqualOperators type when they aren't equality operators. But either way is non-ideal since you shouldn't have to remember to update another definition. So the point is that using the type definition I suggested here works best when the original enum's definition is 1 of:

  1. Will not change much, if ever (or maybe renaming elements)
  2. Will only change by adding elements to the "in" group (the equality operators here)
  3. Will only change by adding elements to the "out" group (the not equality, e.g. less, greater)

You can do this by doing the following:

enum Operator {
  EQUAL = 'EQUAL',
  NOT_EQUAL = 'NOT_EQUAL',
  LESS = 'LESS',
  GREATER = 'GREATER',
}

const EqualOperators = Operator as Omit<typeof Operator, "LESS" | "GREATER">;
type EqualOperators = typeof EqualOperators[keyof typeof EqualOperators];

An enum can be used as both a type and as constant values like below:

enum Operator {
  EQUAL = 'EQUAL',
  NOT_EQUAL = 'NOT_EQUAL',
  LESS = 'LESS',
  GREATER = 'GREATER',
}

function getOperator(op: Operator) {
  if (op == Operator.EQUAL) {
    return "=";
  }
  if (op == Operator.NOT_EQUAL) {
    return "!=";
  }
  if (op == Operator.LESS) {
    return "<";
  }
  if (op == Operator.GREATER) {
    return ">";
  }

  throw new Error('invalid operator');
}

console.log(getOperator(Operator.EQUAL));
console.log(getOperator(Operator.NOT_EQUAL));
console.log(getOperator(Operator.LESS));
console.log(getOperator(Operator.GREATER));
console.log(getOperator('wrong')); // error
console.log(getOperator('wrong' as any)); // works but will throw an error when running

Playground link

Related