npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-safe-enum

v2.0.0

Published

A tiny, type-safe alternative to TypeScript enums. Direct member access, runtime validation, type guards, and zero dependencies.

Readme

ts-safe-enum

A tiny, type-safe alternative to TypeScript enums. Plain objects, full inference, runtime validation — and direct member access that feels just like a native enum.

npm bundle size license

import { defineEnum, type InferValue } from 'ts-safe-enum';

const Status = defineEnum({
  Active:   'active',
  Inactive: 'inactive',
  Pending:  'pending',
});

Status.Active                // 'active'  ← direct access
Status.is(input)             // type guard
Status.assert(input)         // throws on invalid
Status.parse(input)          // { ok, value } | { ok, error }
[...Status]                  // ['active', 'inactive', 'pending']

type Status = InferValue<typeof Status>;
// 'active' | 'inactive' | 'pending'

Why?

TypeScript's built-in enum looks simple but hides real problems:

  • Numeric enums generate reverse mappings and accept any number at runtime
  • String enums behave like nominal types — they reject identical strings from APIs and localStorage
  • const enum gets inlined away and breaks with declaration files, barrel exports, and isolated modules
  • All enums compile to IIFEs that bundlers can't tree-shake

The community alternative is as const objects with union types, but you lose type guards, validation, reverse lookup, and ergonomic member access. You end up writing boilerplate for every enum.

ts-safe-enum gives you a single function that turns a plain object into a fully typed enum with everything you need — zero dependencies, ~2KB minified, frozen by default, and .parse() never throws on any input.

Read the full philosophy: Why I Stopped Using Enums in TypeScript

Install

npm install ts-safe-enum
# or
pnpm add ts-safe-enum
# or
yarn add ts-safe-enum
# or
bun add ts-safe-enum

Quick Start

import { defineEnum, type InferValue } from 'ts-safe-enum';

// Object form — explicit key/value mapping:
const Status = defineEnum({
  Active:   'active',
  Inactive: 'inactive',
  Pending:  'pending',
});

Status.Active   // 'active' — typed as the literal 'active'
Status.size     // 3

type Status = InferValue<typeof Status>;
// 'active' | 'inactive' | 'pending'

// Array form — shorthand when key === value:
const Direction = defineEnum(['Up', 'Down', 'Left', 'Right']);

Direction.Up    // 'Up'

type Direction = InferValue<typeof Direction>;
// 'Up' | 'Down' | 'Left' | 'Right'

API

defineEnum(definition) — Create a type-safe enum

Accepts a plain object or an array of strings. Literal types are inferred automatically — no as const needed.

const HttpStatus = defineEnum({
  Ok:          200,
  NotFound:    404,
  ServerError: 500,
});

HttpStatus.Ok          // 200
HttpStatus.NotFound    // 404

const Role = defineEnum(['Admin', 'Editor', 'Viewer']);
Role.Admin             // 'Admin'

Direct member access

The enum instance exposes each member as a property. This is the same shape you'd expect from a native enum, with full literal-type inference.

Status.Active        // 'active'
Status.Pending       // 'pending'

// Use it anywhere — switch cases, comparisons, default values:
function describe(s: InferValue<typeof Status>) {
  switch (s) {
    case Status.Active:   return 'on';
    case Status.Inactive: return 'off';
    case Status.Pending:  return 'wait';
  }
}

Reserved keys: the following names are reserved and cannot be used as enum members: definition, size, values, keys, entries, is, assert, parse, keyOf. defineEnum throws a clear error at construction time if you use one.

Duplicate values are rejected. Both defineEnum({ A: 'x', B: 'x' }) (object form) and defineEnum(['Up', 'Up']) (array form) throw at construction time. Each enum value must be unique so reverse lookup (keyOf) and iteration stay consistent.

.size — Number of members

Status.size    // 3

.values() — All values as a frozen array

Status.values();
// readonly ['active', 'inactive', 'pending']

.keys() — All keys as a frozen array

Status.keys();
// readonly ['Active', 'Inactive', 'Pending']

.entries() — All [key, value] pairs

Each entry preserves its specific key/value correlation in the type — narrowing on the key narrows the value.

Status.entries();
// readonly (['Active', 'active'] | ['Inactive', 'inactive'] | ['Pending', 'pending'])[]

for (const entry of Status.entries()) {
  switch (entry[0]) {
    case 'Active':
      // entry[1] is narrowed to 'active'
      break;
  }
}

Iteration

The enum is iterable over its values:

for (const value of Status) {
  console.log(value); // 'active', 'inactive', 'pending'
}

[...Status]               // ['active', 'inactive', 'pending']
Array.from(Status)        // same

.is(value) — Type guard

Narrows unknown to the enum's value union:

const input: unknown = getUserInput();

if (Status.is(input)) {
  // input is narrowed to 'active' | 'inactive' | 'pending'
  console.log(`Valid status: ${input}`);
}

.assert(value) — Throw on invalid

Use when you expect the value to be valid and want to fail loudly. Throws a TypeError with a descriptive message and narrows the type after the call. Safe on any input — including BigInt, Symbol, functions, and circular objects.

function load(input: unknown) {
  Status.assert(input);
  // input is now narrowed to 'active' | 'inactive' | 'pending'
  return fetchByStatus(input);
}

.parse(value) — Validate with a Result

Returns { ok: true, value } or { ok: false, error }. .parse() never throws — it's safe to call on any value, including BigInt, Symbol, functions, and objects with circular references. The error is structured so you can build your own messages or report it programmatically.

const result = Status.parse(apiResponse.status);

if (result.ok) {
  console.log(result.value); // typed as 'active' | 'inactive' | 'pending'
} else {
  console.error(result.error.message);
  // 'Invalid enum value: "unknown". Expected one of: "active", "inactive", "pending".'

  result.error.received;  // the value you passed in
  result.error.expected;  // readonly ['active', 'inactive', 'pending']
}

The returned result and its error object are both frozen — they're safe to share across modules without defensive copies.

.keyOf(value) — Reverse lookup

Get the key name for a value. Accepts unknown, so you can pass untrusted input directly without casts.

Status.keyOf('active')    // 'Active'
Status.keyOf('inactive')  // 'Inactive'
Status.keyOf('???')       // undefined  (no cast needed)

Type Helpers

InferValue<T> — Extract the value union

type Status = InferValue<typeof Status>;
// 'active' | 'inactive' | 'pending'

function setStatus(status: Status) { /* ... */ }
setStatus(Status.Active);   // ✅
setStatus('unknown');       // ❌ type error

InferKey<T> — Extract the key union

type StatusKey = InferKey<typeof Status>;
// 'Active' | 'Inactive' | 'Pending'

Real-World Patterns

Validating API responses

const Role = defineEnum({
  Admin:  'admin',
  Editor: 'editor',
  Viewer: 'viewer',
});

type Role = InferValue<typeof Role>;

function handleUser(apiData: { role: string }) {
  const result = Role.parse(apiData.role);

  if (!result.ok) {
    throw new Error(`Unknown role: ${result.error.received}`);
  }

  const role = result.value; // typed as 'admin' | 'editor' | 'viewer'
}

Exhaustive switch statements

const Theme = defineEnum({
  Light:  'light',
  Dark:   'dark',
  System: 'system',
});

type Theme = InferValue<typeof Theme>;

function getBackground(theme: Theme): string {
  switch (theme) {
    case Theme.Light:  return '#ffffff';
    case Theme.Dark:   return '#1a1a1a';
    case Theme.System: return getSystemBackground();
  }
}

Iterating for UI rendering

const Priority = defineEnum({
  Low:      1,
  Medium:   2,
  High:     3,
  Critical: 4,
});

// Render a dropdown
Priority.entries().map(([label, value]) => (
  <option key={value} value={value}>{label}</option>
));

Discriminated-union pattern

.parse() returns a tagged result that narrows naturally with a single if:

const Color = defineEnum({
  Red:   'red',
  Green: 'green',
  Blue:  'blue',
});

const result = Color.parse(userInput);
if (result.ok) {
  applyColor(result.value);    // typed as 'red' | 'green' | 'blue'
} else {
  showError(result.error.message);
}

The shape ({ ok: true, value } | { ok: false, error }) interoperates cleanly with ts-safe-result — wrap the parts in ok() / err() if you want chaining helpers like .map() and .match().

Comparison

| Feature | ts-safe-enum | TS enum | as const + manual | ts-enum-util | | --- | --- | --- | --- | --- | | Tree-shakable | Yes | No (IIFEs) | Yes | Partial | | Direct member access | Yes | Yes | Yes | Partial | | No reverse mapping | Yes | No (numeric) | Yes | No | | Type guard | Yes | No | Manual | Yes | | Validation w/ Result | Yes | No | Manual | No | | Throwing assert | Yes | No | Manual | No | | Reverse lookup | Yes | Numeric only | Manual | Yes | | Iterable | Yes | No | Manual | Partial | | as const required | No | N/A | Yes | N/A | | Bundle size | ~2KB | ~0 (inlined) | 0 | ~3KB |

Migrating from v1

v2 adds direct member access, .assert(), iteration, .size, and a structured parse() error.

Direct access — recommended:

// v1
case Status.definition.Active: ...

// v2 — much cleaner
case Status.Active: ...

parse() error is now an object, not a string:

// v1
if (!result.ok) console.error(result.error);

// v2
if (!result.ok) console.error(result.error.message);
// You can also access result.error.received and result.error.expected

keyOf() no longer requires a cast for untrusted input:

// v1
Status.keyOf(input as never);

// v2
Status.keyOf(input);

Reserved keys: if any of definition, size, values, keys, entries, is, assert, parse, keyOf are used as member names, defineEnum throws at construction time. Rename the offending key.

Duplicate values now throw. v1 silently allowed { A: 'x', B: 'x' } and defineEnum(['Up', 'Up']), which produced inconsistent state (lossy reverse lookup, misleading size). v2 rejects both at construction time. If you relied on aliasing, define the value once and reference it through both names yourself.

parse() and assert() are now safe on any input. v1's error formatting could throw TypeError for BigInt, circular objects, etc. v2 uses a defensive stringifier — .parse() is guaranteed never to throw, and .assert() always throws our TypeError with a useful message.

Part of the ts-safe family

Philosophy

  1. Plain objects over magic. An enum is just an object that TypeScript understands completely — no code generation, no IIFEs, no reverse mappings.
  2. Inference over annotation. You shouldn't need as const, type aliases, or helper types just to define a set of constants.
  3. Validate at boundaries. APIs send strings, localStorage stores strings, URLs contain strings. .is(), .assert(), and .parse() handle the real world.
  4. Frozen by default. Enum instances and the arrays they return are immutable at runtime, not just in the type system.

License

MIT