3

I have an enum containing some arbitrary values and want to automatically map the values of the enum to another enum 1-to-1 (with some arbitrary addition). What would be the most idiomatic way to accomplish this task?

I want to do something like add Element.keys+"Magic" to the Skills enum in a declarative fashion and then have these values available as keys without needing to duplicate the work and add a suffix.

enum Element {
  Air,
  Fire,
  Earth,
  Water
}

-->

enum Skills {
  // Unrelated Skills
  Unarmed,
  LightArmor,
  etc. . .

  // 1-to-1 Mapping
  AirMagic,
  FireMagic,
  EarthMagic,
  WaterMagic
}
6
  • Does this answer your question? Extending Enum in typescript Commented Sep 5, 2022 at 19:30
  • @Blackhole Unfortunately no. Essentially what I'm attempting to do here is establish a single enum and then compose new enums by iterating over the original enum keys. It's not so much a union of enum types as it is a way to reduce duplication. Commented Sep 5, 2022 at 19:39
  • What value should Skills.AirMagic have? Commented Sep 5, 2022 at 20:04
  • @jcalz Because this is an integer enum, the value of Skills.AirMagic would be equal to its integer position in the skills enum at the location the declaration was provided. So if the declarative value was provided after Unarmed and LightArmor the position of AirMagic would be '2' and FireMagic would be '3', etc. . . Commented Sep 5, 2022 at 20:14
  • 1
    Okay, thanks. Well, enums don't compose. You could write your own helper function to produce enum-like objects, as shown in this playground link. Please check that against your use cases to see if it works for you. If so I could write up an answer explaining it; if not, what am I missing? Commented Sep 5, 2022 at 20:15

3 Answers 3

4

Answering my own question in case others are looking to accomplish something similar. . .


While it doesn't seem to be possible to accomplish this task with enums, I have managed something in the same spirit with 'Template Literal Types'.

type Element =
  | "Air"
  | "Fire"
  | "Earth"
  | "Water";
type Skills =
  | "Unarmed"
  | "LightArmor"
  | `${Element}Magic`;

This yields a string union type like so: "Unarmed" | "LightArmor" | "AirMagic" | "FireMagic" | "EarthMagic" | "WaterMagic"


Resources

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

https://www.typescriptlang.org/play?#code/C4TwDgpgBAogNhAthAdsAKuaBeKAiAQQEsAnPKAH3wDFSJyq8YBDE4ACwfwHVngIyAbgCwAKDGhIUAMoBrInDgBnKLjwBVFK2QATLngAyRAObtgBEogD2ZSlAAGAEgDe8JKgxYAvgFlmxogBjexFxUQAzAFcUQOAiKxQofiVgAAoleUUALhlM5QBKKGcxKChAhKUrBAA6OCtjdLz8sS8xMQB6dqhMKWYoFJIiFGModgFocJsoZkjgK3LEMAQ4hOqJCBTUwlI-AMC8ZrCgA

Sign up to request clarification or add additional context in comments.

2 Comments

The code shown here is more similar to your original than using a union of string literals; should I post it as an answer?
@jcalz Go for it!
2

Basically you're asking to compose and extend enum with each other, like extending and composing their inner members, at compile time and with simple set operations.

Shortly: you can't.

Since enums are defined at compile time, their set of member is static and can't be derived from other enums. You can have computed members, but that's for the value they hold in.

For you to access the enum and manipulate like you've asked in the question, you'd need access to metaprogramming onto your enums, i.e. using decorators onto them. But that's currently not possible in Typescript.

My advice would be to use a class and / or a type. Refer to this answer for more.

Comments

1

See the working example on TypeScript Playground. (Quick & Dirty)

While there might be other solutions more suitable for your case, it is indeed possible to solve this your way, but it requires advanced TypeScript. Whether it's worth the effort is up to you.

Solve eventual name conflicts

First you need to rename Element to ElementType as it could conflict with the built-in DOM element (e.g. in a browser application). I wanted to execute it on the TypeScript playground, and had to rename it there:

enum ElementType {
  Air,
  Fire,
  Earth,
  Water,
}

Map Enum Keys by Adding a Suffix

And at this point it is getting a little bit complicated. You need a so called utility function to map enum keys with a suffix and add additional keys like in your case. We create first a type for this mapped enum:

type MappedEnumType<
  T extends Record<string, string | number>,
  Suffix extends string,
> = {
  [key in keyof T as `${string & key}${Suffix}`]: number;
} & Record<string, number>;

Implement the Utility Function

Now we can begin with our utility function:

function createSkillsEnum<
  T extends Record<string, string | number>,
  Suffix extends string,
>(enumObj: T, suffix: string): MappedEnumType<T, Suffix> {
  // Second Enum keys
  const result: Record<string, number> = {
    Unarmed: 100,
    LightArmor: 101,
  };

  // Firs Enum Keys incl. Suffix
  for (const key in enumObj) {
    if (!isNaN(Number(key))) {
      // Filter numeric keys
      const value = enumObj[key];
      if (typeof value === 'string') {
        result[`${value}${suffix}`] = Number(key);
      }
    }
  }

  return result as MappedEnumType<T, Suffix>;
}

Create the new Enum

The rest is easy now, just create the new SkillsEnum with MappedEnumType Keys

const Skills = createSkillsEnum(ElementType, "Magic");

Usage

console.log(Skills);
// Output: { Unarmed: 100, LightArmor: 101, AirMagic: 0, FireMagic: 1, EarthMagic: 2, WaterMagic: 3 }
console.log(Skills.AirMagic); // Output: 0
console.log(Skills.FireMagic); // Output: 1
console.log(Skills.Unarmed); // Output: 100
console.log(Skills.LightArmor); // Output: 101

Handling TypeScript Warnings/Errors

If you get (due to your tsconfig.json settings) an error or a warning line Property 'AirMagic' comes from an index signature, so it must be accessed with ['AirMagic'].ts(4111), and you have to do additional checks:

type SecondEnumType = {
  Unarmed: number;
  LightArmor: number;
};

type MappedEnumType<
  T extends Record<string, string | number>,
  Suffix extends string,
> = {
  [key in keyof T as `${string & key}${Suffix}`]: number;
} & SecondEnumType;

And change the usage a little bit:

// Usage
console.log(Skills);
// Output: { Unarmed: 100, LightArmor: 101, AirMagic: 0, FireMagic: 1, EarthMagic: 2, WaterMagic: 3 }
console.log(Skills['AirMagic']); // Output: 0
console.log(Skills['FireMagic']); // Output: 1
console.log(Skills.Unarmed); // Output: 100
console.log(Skills.LightArmor); // Output: 101

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.