After enough frontend work, the “should we use TypeScript enums?” debate matters less than a more practical problem.
The enum itself is rarely the painful part.
The painful part is keeping **labels, colors, options, filters, and validation logic** in sync.
A status code starts as a simple backend value:
- `0 = draft`
- `1 = published`
- `2 = archived`
Then the UI needs more:
- a human-readable label,
- a translated label,
- a badge color,
- a dropdown option list,
- a table filter list,
- maybe an icon,
- maybe a helper to validate API values.
And suddenly one tiny enum becomes **three, four, or five runtime structures** spread across your codebase.
That’s the problem this post is really about.
## TL;DR
- Native enums and `as const` objects are both useful.
- Neither one gives you a built-in runtime source of truth for UI metadata.
- [`enum-plus`](https://github.com/shijistar/enum-plus) is most interesting when enum-like values need to drive labels, metadata, i18n, and UI lists from one definition.
- If you only need constants and types, you probably don’t need it.
## The usual frontend enum mess
A typical codebase ends up with something like this:
```ts
export enum ArticleStatus {
Draft = 0,
Published = 1,
Archived = 2,
}
export const articleStatusLabels: Record<ArticleStatus, string> = {
[ArticleStatus.Draft]: 'Draft',
[ArticleStatus.Published]: 'Published',
[ArticleStatus.Archived]: 'Archived',
};
export const articleStatusColors: Record<ArticleStatus, string> = {
[ArticleStatus.Draft]: 'gray',
[ArticleStatus.Published]: 'green',
[ArticleStatus.Archived]: 'red',
};
export const articleStatusOptions = [
{ value: ArticleStatus.Draft, label: articleStatusLabels[ArticleStatus.Draft] },
{ value: ArticleStatus.Published, label: articleStatusLabels[ArticleStatus.Published] },
{ value: ArticleStatus.Archived, label: articleStatusLabels[ArticleStatus.Archived] },
];
```
None of this code is wrong.
The problem is that one business concept now lives in multiple runtime structures, and they drift unless someone keeps them aligned.
## `as const` solves one problem — not all of them
A plain `as const` object is still my default when I only need constants and a union type:
```ts
const ArticleStatus = {
Draft: 0,
Published: 1,
Archived: 2,
} as const;
type ArticleStatus = typeof ArticleStatus[keyof typeof ArticleStatus];
```
That works well for type-level constraints.
It stops being enough when the same values also need labels, i18n, colors, option lists, reverse lookups, or extra metadata at runtime.
## What `enum-plus` is actually good at
`enum-plus` is easy to describe as “a drop-in replacement for native enum”, but I think that undersells the most useful part.
`enum-plus` seems most useful when you want **one runtime definition** to drive both typed values and UI-facing data such as labels, metadata, and option lists.
That means your enum can power:
- application logic,
- display labels,
- localization,
- dropdowns and menus,
- table filters,
- metadata lookups,
- validation and lookup helpers.
And it does this in a package that is:
- zero dependency,
- TypeScript-friendly,
- usable in JavaScript too,
- compatible with frontend frameworks and SSR.
## A more useful enum definition
Here’s a practical example:
```ts
import { Enum } from 'enum-plus';
const ArticleStatus = Enum({
Draft: {
value: 0,
label: 'Draft',
color: 'gray',
icon: 'edit',
},
Published: {
value: 1,
label: 'Published',
color: 'green',
icon: 'check-circle',
},
Archived: {
value: 2,
label: 'Archived',
color: 'red',
icon: 'archive',
},
});
```
Now one definition can drive multiple use cases:
```ts
ArticleStatus.Published; // 1
ArticleStatus.label(1); // 'Published'
ArticleStatus.key(1); // 'Published'
ArticleStatus.items; // UI-friendly iterable items
ArticleStatus.findBy('color', 'red') // enum item lookup
ArticleStatus.named.Published.raw; // { value: 1, label: 'Published', color: 'green', icon: 'check-circle' }
```
And if you want direct access to one metadata field:
```ts
ArticleStatus.named.Published.raw.color; // 'green'
```
That may not look dramatic at first glance, but it removes a lot of scattered glue code.
## Why this matters in real frontend code
Suppose your backend returns article rows with a numeric `status` field.
Your UI might need to:
- render a label in a table,
- render a badge color,
- create filter options,
- populate a form select,
- localize the display text.
With the usual approach, these concerns get split across separate maps and helpers.
With a runtime enum definition, they can come from one place.
That doesn’t just save lines of code.
It reduces the number of places where your business vocabulary can silently diverge.
## Native enum vs `as const` vs `enum-plus`
Here’s the practical tradeoff table I wish more articles included:
| Approach | Good for constants/types | Labels and metadata in the same definition | Can generate UI lists from the same definition | Extra maps/helpers needed |
| --- | --- | --- | --- | --- |
| native `enum` | yes | no | no | yes |
| `as const` object | yes | not by itself | not by itself | yes |
| `enum-plus` | yes | yes | yes | fewer |
So no, `enum-plus` is not the right answer for every project.
But it is a strong answer for projects where enum-like values need to drive UI and business display behavior.
## What changed my mind about this category
A lot of enum discussions in TypeScript are framed around language purity:
- should we use enums at all?
- should we prefer unions?
- should we use `as const` objects instead?
Those are valid questions.
But in app development, the bigger problem is often **operational duplication** rather than syntax.
If a single backend code needs to become:
- a readable label,
- a localized label,
- a select option,
- a filter option,
- a badge color,
- a searchable lookup,
then the real design question becomes:
**Where does that information live?**
That’s the question `enum-plus` answers better than native enums do.
## When I would *not* use `enum-plus`
This part matters.
I would **not** use `enum-plus` if:
1. I only need a few constants and a union type.
2. I don’t need runtime labels or metadata.
3. I’m writing a tiny module where plain objects are simpler.
4. My team strongly prefers zero abstraction over convenience helpers.
5. I don’t want another runtime dependency.
6. My team already has a stable plain-object pattern that works well.
7. My enum definitions are generated from API types and I don’t want a wrapper layer.
In those cases, `as const` may be all you need.
But if your codebase keeps rebuilding the same label maps, option arrays, and metadata dictionaries around status-like values, then it’s worth a serious look.
## Why this project seems worth watching
A few things make the repo more credible than a random experiment:
- active release history,
- multiple contributors,
- zero dependencies,
- migration docs,
- plugin system,
- support for frontend-oriented use cases that many teams actually hit.
That combination matters more to me than hype.
## Final thought
I don’t think the strongest pitch for `enum-plus` is:
> “TypeScript enums, but better.”
I think the stronger pitch is:
> “A single runtime source of truth for values your UI needs to display, translate, color, filter, and select.”
I wouldn’t use this everywhere.
But if your frontend repeatedly turns one enum into labels, colors, options, filters, and lookup helpers, then a runtime enum definition can be a reasonable abstraction.
Repo: https://github.com/shijistar/enum-plus