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-symbol-enum

v2.2.0

Published

TypeScript Symbol Enum

Readme

ts-symbol-enum

ts-symbol-enum creates immutable, enum-like objects whose members are distinct symbol values and distinct TypeScript types. Each member also has an arbitrary raw value for parsing, validation, and serialization.

It is a runtime alternative to TypeScript enum for projects using erasableSyntaxOnly: no enum syntax, code generation, or reverse-mapping conventions are involved.

Why use it?

  • Nominal member identity. Status.ACTIVE and OtherStatus.ACTIVE are different symbols, even when their names and raw values are the same.
  • Strong inference. Keys, members, tuples, array indexes, parser results, and raw values are inferred from the declaration as literals.
  • Runtime validation. parse, unparse, and keyOf reject values that do not belong to the enum.
  • Serialization boundary. Keep symbols inside the application and convert to and from strings, numbers, bigints, or other raw values at its edges.
  • Discriminated unions. Symbol members narrow unions in the same way as literal strings or numbers.
  • Iteration. The object is both array-like and map-like, with frozen arrays and standard map iteration methods.
  • No generated code. It is an ordinary ESM function that constructs the object at runtime.

Installation

npm install ts-symbol-enum

The package is ESM-only and currently requires Node.js 22 or newer and TypeScript 5.9 or newer.

Quick start

import { SymbolEnum } from 'ts-symbol-enum';

const Status = SymbolEnum(
  'Status',
  [class { static readonly PENDING: unique symbol; }, 'pending'],
  [class { static readonly APPROVED: unique symbol; }, 'approved'],
  [class { static readonly REJECTED: unique symbol; }, 'rejected'],
);
type Status<K = unknown> = SymbolEnum<typeof Status, K>;

// The type alias is needed to refer to the union of all members as a type.
const anyStatus: Status = Status.PENDING;

// Use symbols inside the program.
function isFinished(status: Status): boolean {
  return status === Status.APPROVED || status === Status.REJECTED;
}

// Parse data received from an API or a file.
const status = Status.parse('approved'); // typeof Status.APPROVED

// Serialize data leaving the program.
const rawStatus = Status.unparse(status); // 'approved'

The declaration also creates numeric properties, so Status[0] is Status.PENDING, and named properties, so Status.PENDING is the preferred form for normal application code.

Select individual members

The call creates the runtime Status object, but that value is not itself a type. The same-name type alias shown in the quick start connects the object to its union of symbol types. Its generic parameter can select members by key:

const oneStatus: Status<'APPROVED'> = Status.APPROVED;
const twoStatuses: Status<'PENDING' | 'REJECTED'> = Status.REJECTED;

// For an individual member, a type alias is not necessary:
const approved: typeof Status.APPROVED = Status.APPROVED;

Without the alias, const anyStatus: Status does not work because Status refers only to the runtime object. Individual member types can always be written with typeof, for example typeof Status.APPROVED | typeof Status.REJECTED.

Discriminated unions

interface Pending {
  status: Status<'PENDING'>;
  createdAt: Date;
}

interface Approved {
  status: Status<'APPROVED'>;
  approvedBy: string;
}

type Request = Pending | Approved;

function describe(request: Request): string {
  if (request.status === Status.PENDING) {
    return request.createdAt.toISOString();
  }
  return request.approvedBy;
}

Raw values

Raw values can be strings, numbers, bigints, undefined, null, NaN, or other values accepted by JavaScript Map. Matching uses Map's SameValueZero comparison, so NaN matches NaN, while 0 and 0n remain different values.

Several members may have the same raw value. parse and tryParse return the first matching member in declaration order; unparse still returns the raw value associated with the specific symbol.

const Kind = SymbolEnum(
  'Kind',
  [class { static readonly FIRST: unique symbol; }, 'same'],
  [class { static readonly SECOND: unique symbol; }, 'same'],
);

Kind.parse('same') === Kind.FIRST; // true
Kind.unparse(Kind.SECOND); // 'same'

API

For an enum declared as Status, the object contains:

| Property or method | Purpose | | --- | --- | | name | The declared enum name. | | size, length | Number of members. | | Status.KEY | The unique symbol for a named member. | | Status[index] | The unique symbol at declaration index. | | keysArray | Frozen tuple of member names. | | valuesArray | Frozen tuple of symbols. | | rawValuesArray | Frozen tuple of raw values. | | entriesArray | Frozen tuple of [key, symbol] entries. | | parse(rawValue) | Return the matching symbol or throw TypeError. | | tryParse(rawValue) | Return the matching symbol or undefined. | | isValidValue(rawValue) | Type guard that checks a raw value. | | unparse(symbol) | Return the symbol's raw value or throw TypeError. | | keyOf(symbol) | Return the symbol's key or throw TypeError. | | has(key) | Check whether a key exists; acts as a key type guard. | | get(key) | Get a symbol by key, or undefined. | | forEach(callback, thisArg?) | Visit entries in declaration order. | | keys() | Iterate over keys. | | values() | Iterate over symbols. | | entries() / [Symbol.iterator]() | Iterate over [key, symbol] pairs. |

The enum object and its exposed arrays and entries are frozen. Map-like iteration follows declaration order.

Declaration constraints

Each entry must be a pair containing:

  1. A class with exactly one static readonly property typed as unique symbol.
  2. The raw value associated with that member.

Member names must be unique and must not be numeric names. These constraints are checked by TypeScript where possible and validated again at runtime.

Why is the declaration syntax so unusual?

The syntax is intentional. TypeScript's unique symbol type is the mechanism that gives every member a distinct, nominal type. However, TypeScript does not let a function infer a fresh unique symbol type from an ordinary string such as 'PENDING'; a type declaration has to introduce that unique symbol.

The empty class body provides that declaration without creating a class value that the library needs to use. Its static property supplies two pieces of compile-time information:

  • the property name (PENDING), which becomes the enum key; and
  • the unique symbol type, which becomes the member's precise type.

At runtime, SymbolEnum reads the class's static property name and creates the actual symbol itself. This keeps the implementation compatible with erasableSyntaxOnly, which permits type-only constructs to disappear during transpilation but rejects TypeScript constructs that require runtime enum transformation. In short, the verbose class is a small type-level adapter that lets the library provide precise unique-symbol types with no code generation.

Limitations

  • The declaration is more verbose than native TypeScript enum syntax.
  • Symbols are not inlined into emitted JavaScript.
  • The package supports ESM imports only; CommonJS is not supported.