Hur man får nästa (eller föregående) enum-värde i Typescript


Jag har en enum som är definierad på detta sätt:

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

//or 

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

Så givet värdet Es.B, vill jag få nästa som är Es.C

Något som Es.B+1 eller getNextEnum(Es.B) eller EnumX.of(Es).next(Es.B) eller ännu enklare? Funktionen bör stödja vilken enum som helst, så inga hårdkodade värden tack.

Observera: Nästa efter sista elementet förväntas vara det första elementet i enum.

Tack

Svar #1

Det är ett sätt att lösa problemet:

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

Och här är lekplatsen för att kolla på det.

Lösningen fungerar eftersom enums i grunden är objekt, du kan iterera nycklar osv.

Och här är en "loopad" lösning som returnerar det första elementet när det sista används som indata (och en lekplats):

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

Svar #2

Istället för att använda enum-funktionen i TypeScript (vilket kan ge oväntat beteende under iteration — se omvända mappningar för numeriska enums i TS-handboken) — kan du använda motsvarande readonly-objekt och typer (använd samma namn för värdet och typen för att efterlikna enum-beteende). Då blir omlindad iteration enkel och förutsägbar:

TS Lekplats

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

Kompilerad JS från lekplatsen:

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

Relaterade artiklar