Skip to content

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: priority is "high" in TypeScript and 3 in the table.
  • Keep the list in a const array 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 with t.enum([...]). A field that is a primary key over a native enum needs a custom type that names the SQL type, so that reload can cast its keys.
  • On MySQL, t.enum is stored the same way, as text or a number.

See also ​