Enums
A field that holds one of a fixed set of values.
Setup
The code of this recipe depends on these declarations: the imports, the schema classes, the repo, and the migration that creates the tables. Run the recipe in an empty database of its own: each recipe creates its own tables, and some have the same names (posts) with other columns.
ts
import { postgres } from "@egon-orm/postgres";
import { cast, createRepo, from, Schema, t } from "egon-orm";
import { createMigrator, defineMigration } from "egon-orm/migration";
const adapter = postgres({ connectionString: process.env.DATABASE_URL });
const repo = createRepo({ adapter });Declaring one
ts
export const STATUSES = ["draft", "review", "published"] as const;
class Post extends Schema("posts", {
id: t.integer().primaryKey().dbGenerated(),
status: t.enum(STATUSES).default("draft"), // text: "draft" | "review" | "published"
priority: t.enum({ low: 1, normal: 2, high: 3 }).default("normal"), // integer, read as its key
}) {
static changeset(post: Post | typeof Post, params: unknown) {
return cast(post, params, ["status", "priority"]);
}
}
export type Status = Post["status"]; // "draft" | "review" | "published"t.enum([...])stores the value as text. The TypeScript type is the union of the values.t.enum({ key: value })stores a number (or another string) and gives your code the key:priorityis"high"in TypeScript and3in the table.- Keep the list in a
constarray when other code needs it (a select box, an API schema).
The column
A check constraint keeps other values out of the table, whatever writes to it:
ts
export const posts = defineMigration({
up(m) {
m.createTable("posts", (table) => {
table.id({ type: "integer" });
table.enumText("status", STATUSES).notNull().default("draft"); // text with posts_status_check
table.integer("priority").notNull().default(2);
});
},
down(m) {
m.dropTable("posts");
},
});t.enumText names the constraint posts_status_check. A changeset never sends an unknown value (casting rejects it first), so the constraint guards against other writers.
Using it
The examples below start from these rows, after creating the tables:
ts
await createMigrator({
adapter,
migrations: [{ version: 20261021000301, name: "enums_posts", migration: posts }],
}).up();ts
const invalid = Post.changeset(Post, { status: "archived" });
invalid.errorMessages(); // => { status: ["is invalid"] }
const urgent = await repo.insertOrThrow(Post.changeset(Post, { status: "review", priority: "high" }));
urgent.priority; // => "high"
const waiting = await repo.all(
from(Post, "p").where(({ p }) => p.status.in(["draft", "review"]).and(p.priority.eq("high"))),
);Comparing with a value outside the list (p.status.eq("archived")) is a compile error.
Good to know
- Adding a value is a change to the code and a migration that replaces the check constraint. Removing one needs the rows that hold it changed first.
- Native Postgres enums (
create type mood as enum (…)) work too: create the type in a migration (m.execute) and declare the field witht.enum([...]). A field that is a primary key over a native enum needs a custom type that names the SQL type, so thatreloadcan cast its keys. - On MySQL,
t.enumis stored the same way, as text or a number.

