TypeScript Enum with Multiple Values (4 Patterns That Work)

A TypeScript enum with multiple values isn’t something the language supports directly: each member holds exactly one number or string. The clean fix is to keep the enum for the name and attach the extra values with a typed lookup object, Record<YourEnum, Details>:

const SHIPPING: Record<ShippingMethod, ShippingInfo> = {
  [ShippingMethod.Ground]: { label: "Ground", priceUSD: 5.99, days: 5 },
  // ...one entry per member, or the compiler complains
};

My test bench for this one was TypeScript 7.0.2 and Node.js v22.22.2 with --strict on, and I copied each console result straight from those runs, error messages included.

Below you’ll find why the obvious syntax fails, the Record lookup, an as const object, a class-based enum with methods, and bit flags for storing several members in one variable.

Why can’t a TypeScript enum member hold multiple values?

Enum members compile to plain properties on a JavaScript object, and numeric members also get a reverse mapping back to the name. So each name can hold just one value. If enums are new to you, my TypeScript enums guide covers the basics first.

A common attempt is ClubA = 100 | 102. It compiles, but | here is bitwise OR, not “or”:

enum Club {
  ClubA = 100 | 102,   // looks like "100 or 102"
  ClubB = 200,
}

console.log("Club.ClubA =", Club.ClubA);
console.log("Club[100]  =", Club[100]);
console.log("Club[102]  =", Club[102]);

Output:

Club.ClubA = 102
Club[100]  = undefined
Club[102]  = ClubA

TypeScript evaluated 100 | 102 to one number, 102, so Club[100] is undefined. An array initializer is rejected outright, and that’s the first error in the next screenshot.

Map each enum member to an object with Record

This is the pattern I reach for most. The enum stays a list of names, and a Record keyed by the enum holds the label, price and delivery days for each one. Here’s what the compiler does when you get it wrong:

enum Region {
  West = ["CA", "OR", "WA"],
}

enum ShippingMethod {
  Ground = "GROUND",
  TwoDay = "TWO_DAY",
  Overnight = "OVERNIGHT",
}

interface ShippingInfo {
  label: string;
  priceUSD: number;
  days: number;
}

const SHIPPING: Record<ShippingMethod, ShippingInfo> = {
  [ShippingMethod.Ground]: { label: "Ground", priceUSD: 5.99, days: 5 },
  [ShippingMethod.TwoDay]: { label: "2-Day Air", priceUSD: 12.5, days: 2 },
};

Output:

enum_errors.ts(2,10): error TS18033: Type 'string[]' is not assignable to type 'number' as required for computed enum member values.
enum_errors.ts(17,7): error TS2741: Property 'OVERNIGHT' is missing in type '{ GROUND: { label: string; priceUSD: number; days: number; }; TWO_DAY: { label: string; priceUSD: number; days: number; }; }' but required in type 'Record<ShippingMethod, ShippingInfo>'.
Command Prompt output of tsc rejecting an array as a TypeScript enum initializer and a Record missing one enum member
An array can’t be an enum value, and the Record has no OVERNIGHT entry, so tsc refuses both.

That second error is the real win: add a member to the enum later, and every Record keyed by it stops compiling until you fill in the new entry. My TypeScript Record type explained post goes deeper on how Record works.

With all three entries in place, you can loop over the members and read every value:

enum ShippingMethod {
  Ground = "GROUND",
  TwoDay = "TWO_DAY",
  Overnight = "OVERNIGHT",
}

interface ShippingInfo {
  label: string;
  priceUSD: number;
  days: number;
}

const SHIPPING: Readonly<Record<ShippingMethod, ShippingInfo>> = {
  [ShippingMethod.Ground]: { label: "Ground", priceUSD: 5.99, days: 5 },
  [ShippingMethod.TwoDay]: { label: "2-Day Air", priceUSD: 12.5, days: 2 },
  [ShippingMethod.Overnight]: { label: "Overnight", priceUSD: 29.95, days: 1 },
};

for (const method of Object.values(ShippingMethod)) {
  const { label, priceUSD, days } = SHIPPING[method];
  console.log(`${method.padEnd(10)} ${label.padEnd(10)} $${priceUSD.toFixed(2).padStart(5)}  ${days} day(s)`);
}

function shippingCost(method: ShippingMethod, subtotalUSD: number): number {
  if (method === ShippingMethod.Ground && subtotalUSD >= 50) return 0;   // free ground over $50
  return SHIPPING[method].priceUSD;
}

console.log("Ground on a $64.20 order: $" + shippingCost(ShippingMethod.Ground, 64.2).toFixed(2));
console.log("2-Day on a $64.20 order:  $" + shippingCost(ShippingMethod.TwoDay, 64.2).toFixed(2));

Output:

GROUND     Ground     $ 5.99  5 day(s)
TWO_DAY    2-Day Air  $12.50  2 day(s)
OVERNIGHT  Overnight  $29.95  1 day(s)
Ground on a $64.20 order: $0.00
2-Day on a $64.20 order:  $12.50
Command Prompt output of a TypeScript enum with multiple values stored in a Record: label, USD price and delivery days per member
Each ShippingMethod member now carries a label, a price in dollars and a delivery time.

Object.values() is safe on a string enum because there’s no reverse mapping to filter out. Wrapping the type in Readonly means you can’t reassign an entry by accident. For more loop styles, see how to iterate over a Record in TypeScript.

Use an as const object instead of an enum

Plenty of teams skip the enum entirely. as const keeps the values literal, and satisfies checks every entry has the same shape:

const STATES = {
  CA: { name: "California", capital: "Sacramento", salesTax: 7.25 },
  TX: { name: "Texas", capital: "Austin", salesTax: 6.25 },
  NY: { name: "New York", capital: "Albany", salesTax: 4.0 },
} as const satisfies Record<string, { name: string; capital: string; salesTax: number }>;

type StateCode = keyof typeof STATES;   // "CA" | "TX" | "NY"

function salesTax(code: StateCode, amountUSD: number): string {
  const s = STATES[code];
  return `${s.name}: $${((amountUSD * s.salesTax) / 100).toFixed(2)} state tax on $${amountUSD.toFixed(2)}`;
}
console.log(salesTax("TX", 249.99));

function isStateCode(value: string): value is StateCode {
  return Object.hasOwn(STATES, value);
}

for (const input of ["CA", "NY", "FL"]) {
  console.log(input, "->", isStateCode(input) ? STATES[input].capital : "not supported");
}

Output:

Texas: $15.62 state tax on $249.99
CA -> Sacramento
NY -> Albany
FL -> not supported

keyof typeof STATES gives you the union "CA" | "TX" | "NY", so a typo like "TXX" fails to compile. The isStateCode guard turns untrusted input into that union, the same trick explained in my post on the is keyword in TypeScript.

Curious why I used satisfies instead of a type annotation? It keeps the literal types; see satisfies vs as in TypeScript.

Build a class-based enum with multiple properties

When each value also needs behavior, a class with a private constructor and static instances acts like a richer enum. Nobody outside the class can create a fourth plan:

class PaymentPlan {
  static readonly Monthly = new PaymentPlan("MONTHLY", "Monthly", 19.99, 1);
  static readonly Quarterly = new PaymentPlan("QUARTERLY", "Quarterly", 54.99, 3);
  static readonly Annual = new PaymentPlan("ANNUAL", "Annual", 199.0, 12);

  private constructor(
    readonly code: string,
    readonly label: string,
    readonly priceUSD: number,
    readonly months: number,
  ) {}

  get perMonthUSD(): number {
    return this.priceUSD / this.months;
  }

  static values(): PaymentPlan[] {
    return [PaymentPlan.Monthly, PaymentPlan.Quarterly, PaymentPlan.Annual];
  }

  static fromCode(code: string): PaymentPlan | undefined {
    return PaymentPlan.values().find((p) => p.code === code);
  }

  toString(): string {
    return `${this.label} plan, $${this.priceUSD.toFixed(2)}`;
  }
}

for (const plan of PaymentPlan.values()) {
  console.log(`${plan.code.padEnd(10)} $${plan.perMonthUSD.toFixed(2)} per month`);
}
console.log(`${PaymentPlan.fromCode("ANNUAL")}`);
console.log("WEEKLY ->", PaymentPlan.fromCode("WEEKLY"));
console.log("same instance:", PaymentPlan.fromCode("ANNUAL") === PaymentPlan.Annual);

Output:

MONTHLY    $19.99 per month
QUARTERLY  $18.33 per month
ANNUAL     $16.58 per month
Annual plan, $199.00
WEEKLY -> undefined
same instance: true
Command Prompt output of a TypeScript class-based enum where each payment plan has a code, label, USD price and per-month getter
Each plan carries several values plus a computed per-month price, and fromCode looks one up by its string code.

Because every plan is a single shared instance, you can compare with === just like enum members. For when a class beats a plain type, read TypeScript class vs interface.

Store multiple enum values in one variable with bit flags

Sometimes “multiple values” means one user holds several permissions at once. Give each member a power of two, combine members with |, and test them with &:

enum Permission {
  None = 0,
  Read = 1 << 0,     // 1
  Write = 1 << 1,    // 2
  Delete = 1 << 2,   // 4
  Share = 1 << 3,    // 8
  Editor = Read | Write,
  Admin = Read | Write | Delete | Share,
}

function has(set: Permission, flag: Permission): boolean {
  return (set & flag) === flag;
}

function names(set: Permission): string[] {
  return Object.entries(Permission)
    .filter(([, v]) => typeof v === "number" && v !== 0 && (v & (v - 1)) === 0 && (set & v) === v)
    .map(([k]) => k);
}

let user: Permission = Permission.Read | Permission.Share;
console.log("value:", user, "->", names(user).join(" + "));
console.log("can share: ", has(user, Permission.Share));
console.log("can delete:", has(user, Permission.Delete));

user |= Permission.Write;      // add a flag
user &= ~Permission.Share;     // remove a flag
console.log("after edit:", user, "->", names(user).join(" + "));
console.log("is Editor: ", has(user, Permission.Editor));
console.log("Admin:", Permission.Admin, "->", names(Permission.Admin).join(" + "));

Output:

value: 9 -> Read + Share
can share:  true
can delete: false
after edit: 3 -> Read + Write
is Editor:  true
Admin: 15 -> Read + Write + Delete + Share
Command Prompt output of TypeScript enum bit flags combining Read, Write and Share permissions with bitwise OR and AND
One number, 9, holds Read and Share; adding Write and removing Share turns it into an Editor.

Bitwise operators work on 32-bit integers, so you get at most 31 safe flags. Never give two flags the same bit; my post on how to avoid duplicate enum values in TypeScript shows why that bites later.

Which TypeScript enum pattern should you choose?

Here’s how I pick. Whichever you choose, you’ll often look values up by key, so keep checking if a key exists in a TypeScript object handy.

PatternBest forWatch out for
Enum + RecordFixed sets with a few fields eachKeep the Record next to the enum
as const objectConfig-driven data, no emitted enumUse a type guard for outside input
Class-based enumValues that need methodsMore code to maintain
Bit flag enumSeveral members active at onceMax 31 flags, powers of two only

Want to keep going with TypeScript types? These are good next reads:

Frequently asked questions

Can a TypeScript enum member have more than one value?

No. Each member holds one number or string. Attach extra values with a Record<Enum, Details>, an as const object or a class with static instances.

Why does ClubA = 100 | 102 give 102?

The | in an enum initializer is bitwise OR, so TypeScript computes one number. 100 | 102 is 102, and 100 is lost.

How do I give an enum member a label and a price?

Create an interface for the details and a Record<YourEnum, Details>. The compiler then requires an entry for every member.

How do I store several enum values in one variable?

Use a numeric enum whose members are powers of two. Combine them with |, test with (set & flag) === flag and remove with &= ~flag.

Should I use an enum or an as const object?

Both work. An as const object emits nothing extra and gives you a string union, while an enum gives you a named type and reverse mapping for numbers.

Where are TypeScript enums documented?

See the Enums handbook page on typescriptlang.org.