String enums that know where they belong.
Documentation · Getting started · API reference · Examples · ESLint
Enumwaii gives a closed string vocabulary a type-level owner. Members remain ordinary strings at runtime, while TypeScript rejects raw look-alikes and same-text members from distinct value sets.
import { em } from "enumwaii";
const workspaceRoles = em(["ADMIN", "MEMBER"]);
const billingRoles = em(["ADMIN", "VIEWER"]);
const WORKSPACE_ROLE = workspaceRoles.enum;
const BILLING_ROLE = billingRoles.enum;
type WorkspaceRole = (typeof workspaceRoles)["~type"];
function canInviteToWorkspace(role: WorkspaceRole): boolean {
return role === WORKSPACE_ROLE.ADMIN;
}
canInviteToWorkspace(WORKSPACE_ROLE.ADMIN); // valid
canInviteToWorkspace(BILLING_ROLE.ADMIN); // TypeScript error: wrong owner
canInviteToWorkspace("ADMIN"); // TypeScript error: raw string
declare const externalRole: unknown;
canInviteToWorkspace(workspaceRoles.parse(externalRole)); // validate, then trustUse enumwaii for authored, behavior-driving vocabularies such as permissions, workflow states, event kinds, access levels, provider codes, form options, and persisted statuses. Keep free-form user text, names, identifiers, and intentionally open-ended values as plain strings.
npm install enumwaiiOR
pnpm add enumwaiiOR
yarn add enumwaiiOR
bun add enumwaiiOR
deno add npm:enumwaiiThe package ships ESM and CommonJS entry points with declarations and source maps. Node.js 18 or newer is supported; the ESM build is also tested under Bun, Deno, and Cloudflare Workers without Node compatibility flags.
- Raw literals cannot enter an owned enum position accidentally.
- Same-text members from distinct value sets remain incompatible.
- Members serialize naturally through JSON, URLs, forms, databases, and structured cloning.
parse,safeParse, andisvalidate unknown values where they enter.- Every declaration implements Standard Schema v1 directly.
pick,omit,extend, andcombinepreserve or deliberately create identity;deriveandderiveTobuild exhaustive data from owned members.- Optional ESLint rules enforce authoring conventions that TypeScript cannot express reliably.
The ownership brand exists only in TypeScript. Assertions, any, plain JavaScript, and unvalidated external data can still bypass it, so parse JSON, route values, form fields, database rows, provider payloads, and agent output at their boundary.
declare const input: unknown;
const strictRole = workspaceRoles.parse(input);
canInviteToWorkspace(strictRole);
const result = workspaceRoles.safeParse(input);
if (result.success) {
canInviteToWorkspace(result.value);
} else {
throw result.error;
}Parsing also distinguishes absence from malformed input:
workspaceRoles.parse(input, { default: WORKSPACE_ROLE.MEMBER });
workspaceRoles.parse(input, { fallback: WORKSPACE_ROLE.MEMBER });default handles only null and undefined; fallback handles every other invalid value. See runtime boundaries for complete behavior and error handling.
An enumwaii declaration already is a Standard Schema schema. Compatible form, RPC, routing, and validation libraries can consume workspaceRoles directly, without another wrapper or validator dependency.
Use an adapter only when an API specifically requires its native schema type:
import { emToZodSchema } from "enumwaii/zod";
export const workspaceRoleSchema = emToZodSchema(workspaceRoles);Zod and Valibot are optional peers with separate enumwaii/zod and enumwaii/valibot entry points. Read schemas and adapters for installation and type-preserving examples.
const adminRoles = workspaceRoles.pick([WORKSPACE_ROLE.ADMIN]);
adminRoles.is(WORKSPACE_ROLE.ADMIN);
const labels = workspaceRoles.derive(
[WORKSPACE_ROLE.ADMIN, "Administrator"],
[WORKSPACE_ROLE.MEMBER, "Member"],
);
labels.get(WORKSPACE_ROLE.ADMIN);Tuple derivation is deliberate: object keys erase branded provenance in TypeScript. The derivation guide covers contextual output types, callback transforms, deriveTo, and arrays of target members.
Use .enum for normal application code. .rawEnum, .rawValues, and .cases are narrow integration and discriminated-union escape hatches, not parallel general-purpose APIs. See member surfaces before adopting one.
The lightweight preset requires no TypeScript project and enforces declaration casing:
import enumwaii from "eslint-plugin-enumwaii";
export default [...enumwaii.configs["flat/recommended"]];The flat/recommended-type-checked preset additionally catches direct member view access, raw comparisons, raw subset members, .cases misuse, and unsafe union-narrowing patterns. It requires TypeScript parser services; follow the complete ESLint setup.
| Area | Checked-in examples |
|---|---|
| Web applications | Next.js, TanStack Start + Solid, and Vue |
| Servers | Hono, Elysia, oRPC, Effect, and NestJS |
| Runtimes | Node.js, Bun, Deno, and Cloudflare Workers through the shared Hono boundary contract |
| Data and everyday UI | Drizzle + PGlite, Mongoose, TanStack Form, native forms, TanStack Table, URL state, persistence hydration, request validation, and visible failure paths across the examples above |
Every example is independently runnable and exercises invalid or ambiguous input instead of demonstrating only a happy path. Open the example catalog for commands and hosted StackBlitz links.
- Getting started — install, declare, extract, and parse.
- API essentials — the practical surface map.
- Branding and identity — guarantees, tradeoffs, and why the brand is required.
- Documentation map — API guides, validation, integrations, and tooling.
- Generated API reference — signatures generated from public JSDoc.
- AI agents —
llms.txt, the full agent brief, and the packaged skill.
| Package | Purpose |
|---|---|
enumwaii |
Runtime declarations, Standard Schema, adapters, types, and derivation helpers. |
eslint-plugin-enumwaii |
Syntax-only and type-aware authoring rules. |
Read CONTRIBUTING.md for setup, tests, documentation, and Changesets. Release and repository administration live in MAINTAINERS.md. Usage questions belong in SUPPORT.md, and security reports should follow SECURITY.md.
MIT © CatOfJupit3r