简体   繁体   中英

How to get next (or previous) enum value in Typescript

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

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM