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>'.

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

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

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

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.
| Pattern | Best for | Watch out for |
|---|---|---|
Enum + Record | Fixed sets with a few fields each | Keep the Record next to the enum |
as const object | Config-driven data, no emitted enum | Use a type guard for outside input |
| Class-based enum | Values that need methods | More code to maintain |
| Bit flag enum | Several members active at once | Max 31 flags, powers of two only |
Want to keep going with TypeScript types? These are good next reads:
- Key-value pairs in TypeScript
- Convert an object to an array in TypeScript
- Iterate over objects in TypeScript
- Compare strings in TypeScript
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.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn