Home > Software engineering >  How to get next (or previous) enum value in Typescript
How to get next (or previous) enum value in Typescript

Time:09-03

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

CodePudding user response:

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

CodePudding user response:

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

  • Related