1

I have an enum which is defined like this:

enum Ed {
  up,
  down,
  left,
  right,
}

//or 

enum Es {
  A = "a",
  B = "b",
  C = "c",
}

So given value Es.B, I want to get the next one which is Es.C

Something like Es.B+1 or getNextEnum(Es.B) or EnumX.of(Es).next(Es.B) or even simpler? Function should support any enum, so no hardcoded values please.

Note: Next of last element is expected to be the first element of enum.

Thanks

husayt
  • 14,553
  • 8
  • 53
  • 81
  • THere is one thing you should be aware of: `enum` is a hash map data stucture, where keys are unordered. IF you target V8, most probably `Object.values` in 99% will give you the expected order, but there is no guarantee in javascript spec about keys order. Hence, if you want to be 100% safe, I think it worth using tuples, for instance `const Es=['a','b','c'] as const` – captain-yossarian from Ukraine Sep 03 '22 at 08:18
  • 1
    @captain-yossarianfromUkraine Thanks for commenting about that: object iteration order is guaranteed to be deterministic since ES2015: [here’s](https://stackoverflow.com/a/5525820/438273) an SO post with details. – jsejcksn Sep 03 '22 at 11:31
  • @jsejcksn thank you very much for the info, was not aware about it – captain-yossarian from Ukraine Sep 03 '22 at 11:34

2 Answers2

3

That's one way to solve the problem:

class EnumX {
  static of<T extends object>(e: T) {
    const values = Object.values(e)
    const map = new Map(values.map((k, i) => [k, values[i + 1]]));
    return {
      next: <K extends keyof T>(v: T[K]) => map.get(v)
    }
  }
}

And here is playground to check it out.

The solution works because enums are basically objects, you can iterate keys etc.

And here is "looped" solution which returns first element when last one is used as input (and a playground):

class EnumX {
  static of<T extends object>(e: T) {
    const values = Object.values(e)
    return {
      next: <K extends keyof T>(v: T[K]) => values[(values.indexOf(v) + 1) % values.length]
    }
  }
}
alx
  • 2,314
  • 2
  • 18
  • 22
  • 1
    This is very neat. One thing I forgot to mention (updated question) is that the next of the last element should be the first element. Thanks – husayt Sep 03 '22 at 00:24
  • "looped" solution does not work correctly for the number based enum. For example, try this enum Es { A = 1, B = 2, C = 3 } – Alex Ryltsov Jan 11 '23 at 10:27
1

Instead of using the enum feature in TypeScript (which might have unexpected behavior during iteration — see reverse mappings for numeric enums in the TS handbook) — you can use the equivalent readonly objects and types (use the same name for the value and the type to emulate enum behavior). Then wrapped iteration is straightforward and predictable:

TS Playground

type Values<T> = T[keyof T];

type AnyEnum<Values extends string | number = string | number> =
  Readonly<Record<string, Values>>;

function getOffsetValue <Enum extends AnyEnum>(
  e: Enum,
  current: Values<Enum>,
  distance: number,
): Values<Enum> {
  const values = Object.values(e) as Values<Enum>[];

  // You could just do this:
  // const index = (values.indexOf(current) + distance) % values.length;

  let index = values.indexOf(current);
  // But it's safer to validate at runtime:
  if (index === -1) throw new TypeError('Value not found');
  index = (index + distance) % values.length;
  return values[index < 0 ? values.length + index : index]!;
}

function getNextValue <Enum extends AnyEnum>(
  e: Enum,
  current: Values<Enum>,
): Values<Enum> {
  return getOffsetValue(e, current, 1);
}

function getPreviousValue <Enum extends AnyEnum>(
  e: Enum,
  current: Values<Enum>,
): Values<Enum> {
  return getOffsetValue(e, current, -1);
}


// Your enums as objects created using "const assertions" —
// the result is readonly properties and literal value types:

const Ed = {
  up: 0,
  down: 1,
  left: 2,
  right: 3,
} as const;

type Ed = Values<typeof Ed>;
   //^? type Ed = 0 | 1 | 2 | 3

const Es = {
  A: 'a',
  B: 'b',
  C: 'c',
} as const;

type Es = Values<typeof Es>;
   //^? type Es = "a" | "b" | "c"


// Usage:

// Numeric enum:
const left = getOffsetValue(Ed, Ed.down, 5);
    //^? 0 | 1 | 2 | 3
console.log('left === Ed.left', left === Ed.left); // true

const up = getNextValue(Ed, Ed.right);
console.log('up === Ed.up', up === Ed.up); // true

const right = getPreviousValue(Ed, Ed.up);
console.log('right === Ed.right', right === Ed.right); // true


// String enum:
const b = getOffsetValue(Es, Es.A, -2);
console.log('b === Es.B', b === Es.B); // true

const a = getNextValue(Es, Es.C);
    //^? "a" | "b" | "c"
console.log('a === Es.A', a === Es.A); // true

const c = getPreviousValue(Es, Es.A);
console.log('c === Es.C', c === Es.C); // true


// Full iteration example from your dataset:
for (const [name, e] of [['Ed', Ed], ['Es', Es]] as [string, AnyEnum][]) {
  console.log(`${name}:`);
  for (const current of Object.values(e)) {
    const previous = getPreviousValue(e, current);
    const next = getNextValue(e, current);
    console.log(`${JSON.stringify(previous)} <- ${JSON.stringify(current)} -> ${JSON.stringify(next)}`);
  }
}


// Examples of compiler safety:

getNextValue(Ed, Es.A); /*
                 ~~~~
Argument of type '"a"' is not assignable to parameter of type 'Ed'.(2345) */

getNextValue(Es, 'c'); // ok
getNextValue(Es, 'd'); /*
                 ~~~
Argument of type '"d"' is not assignable to parameter of type 'Es'.(2345) */


// Where runtime validation matters:

/*
This object fits the constraint criteria, but wasn't created with literal
properties — it uses an index signature. A compiler error doesn't occur
because `5` is assignable to `number` (the types are ok), but the input data
is obviously invalid (it's out of range). If there were no runtime validation
to throw an exception on invalid inputs, the behavior would be unexpected:
*/
getNextValue({a: 1, b: 2} as Record<string, number>, 5);

Compiled JS from the playground:

"use strict";
function getOffsetValue(e, current, distance) {
    const values = Object.values(e);
    // You could just do this:
    // const index = (values.indexOf(current) + distance) % values.length;
    let index = values.indexOf(current);
    // But it's safer to validate at runtime:
    if (index === -1)
        throw new TypeError('Value not found');
    index = (index + distance) % values.length;
    return values[index < 0 ? values.length + index : index];
}
function getNextValue(e, current) {
    return getOffsetValue(e, current, 1);
}
function getPreviousValue(e, current) {
    return getOffsetValue(e, current, -1);
}
// Your enums as objects created using "const assertions" —
// the result is readonly properties and literal value types:
const Ed = {
    up: 0,
    down: 1,
    left: 2,
    right: 3,
};
//^? type Ed = 0 | 1 | 2 | 3
const Es = {
    A: 'a',
    B: 'b',
    C: 'c',
};
//^? type Es = "a" | "b" | "c"
// Usage:
// Numeric enum:
const left = getOffsetValue(Ed, Ed.down, 5);
//^? 0 | 1 | 2 | 3
console.log('left === Ed.left', left === Ed.left); // true
const up = getNextValue(Ed, Ed.right);
console.log('up === Ed.up', up === Ed.up); // true
const right = getPreviousValue(Ed, Ed.up);
console.log('right === Ed.right', right === Ed.right); // true
// String enum:
const b = getOffsetValue(Es, Es.A, -2);
console.log('b === Es.B', b === Es.B); // true
const a = getNextValue(Es, Es.C);
//^? "a" | "b" | "c"
console.log('a === Es.A', a === Es.A); // true
const c = getPreviousValue(Es, Es.A);
console.log('c === Es.C', c === Es.C); // true
// Full iteration example from your dataset:
for (const [name, e] of [['Ed', Ed], ['Es', Es]]) {
    console.log(`${name}:`);
    for (const current of Object.values(e)) {
        const previous = getPreviousValue(e, current);
        const next = getNextValue(e, current);
        console.log(`${JSON.stringify(previous)} <- ${JSON.stringify(current)} -> ${JSON.stringify(next)}`);
    }
}
jsejcksn
  • 27,667
  • 4
  • 38
  • 62