typed_enums
Use Rails enums in JavaScript and TypeScript without duplicating option lists.
typed_enums is a Rails enum generator for JavaScript and TypeScript frontends. It exports Active Record enums to JavaScript constants and TypeScript declaration types, giving any frontend a Rails-like way to consume enum values.
Rails routes have js-routes. Rails serializers have serializer type generators. Rails enums now have typed_enums.
What It Does
typed_enums scans loaded Active Record models, reads defined_enums, and writes one generated JavaScript module plus a TypeScript declaration file:
app/javascript/lib/enums.js
app/javascript/lib/enums.d.ts
It works with React, Vue, Svelte, API-only Rails apps, plain JavaScript, or any TypeScript frontend. The primary runtime output is plain JavaScript. No frontend framework, bundler, or TypeScript runtime package is required by the gem itself.
What It Does Not Do
This gem is not a serializer, OpenAPI generator, route helper generator, form builder, or enum replacement. It does not generate React components, Vue components, Svelte components, migrations, or runtime enum editing tools.
Installation
Add the gem:
# Gemfile
gem "typed_enums"
Install and generate the initializer:
bundle install
bin/rails generate typed_enums:install
bin/rails typed_enums:generate
Rails Setup
Given a Rails model:
class Task < ApplicationRecord
enum :work_priority, {
priority_1: 0,
priority_2: 1,
priority_3: 2,
priority_4: 3
}
end
The generated JavaScript can be imported from the generated module:
import { Task } from "@/lib/enums";
Task.workPriorities;
Generated JavaScript
The gem writes one JavaScript module containing every model enum:
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
// enum-schema-sha256: abc123
export const Task = {
workPriorities: ["priority_1", "priority_2", "priority_3", "priority_4"],
};
It also writes enums.d.ts for TypeScript users:
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
// enum-schema-sha256: abc123
export declare const Task: {
readonly workPriorities: readonly ["priority_1", "priority_2", "priority_3", "priority_4"];
};
export type TaskWorkPriority = (typeof Task.workPriorities)[number];
Naming
Rails exposes enum mappings through pluralized methods, such as Task.work_priorities. typed_enums mirrors that convention in camelCase:
| Rails enum attribute | Rails mapping method | TypeScript property | TypeScript type |
|---|---|---|---|
ticket_status |
ticket_statuses |
ticketStatuses |
TaskTicketStatus |
work_priority |
work_priorities |
workPriorities |
TaskWorkPriority |
fix_priority |
fix_priorities |
fixPriorities |
TaskFixPriority |
severity |
severities |
severities |
TaskSeverity |
urgency |
urgencies |
urgencies |
TaskUrgency |
This naming is intentionally not configurable in v1.
Type Usage
import { Task, type TaskWorkPriority } from "@/lib/enums";
function setPriority(priority: TaskWorkPriority) {
return priority;
}
React
import { Task, type TaskWorkPriority } from "@/lib/enums";
export function PrioritySelect(props: {
value: TaskWorkPriority;
onChange: (value: TaskWorkPriority) => void;
}) {
return (
<select value={props.value} onChange={(event) => props.onChange(event.target.value as TaskWorkPriority)}>
{Task.workPriorities.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
);
}
Vue
<script setup lang="ts">
import { Task, type TaskWorkPriority } from "@/lib/enums";
const model = defineModel<TaskWorkPriority>();
</script>
<template>
<select v-model="model">
<option v-for="value in Task.workPriorities" :key="value" :value="value">
{{ value }}
</option>
</select>
</template>
Svelte Or Plain TypeScript
import { Task, type TaskWorkPriority } from "@/lib/enums";
export const priorityOptions = Task.workPriorities.map((value: TaskWorkPriority) => ({
value,
label: value,
}));
Plain JavaScript
import { Task } from "@/lib/enums";
export const priorityOptions = Task.workPriorities.map((value) => ({
value,
label: value,
}));
i18n Labels
The generated values are intentionally raw enum values. Use your frontend i18n layer to label them:
import { Task } from "@/lib/enums";
Task.workPriorities.map((value) => ({
value,
label: t(`enums.task.workPriorities.${value}`),
}));
Example keys:
{
"enums": {
"task": {
"workPriorities": {
"priority_1": "P1",
"priority_2": "P2"
}
}
}
}
Configuration
The installer creates config/initializers/typed_enums.rb:
# frozen_string_literal: true
TypedEnums.configure do |config|
config.output_dir = "app/javascript/lib"
config.root_model_class = "ApplicationRecord"
config.auto_generate_in_development = true
config.watch_models_in_development = true
end
Keep the output directory isolated and import generated code from that directory. The generator never appends to user-owned frontend files.
Rake Tasks
Generate files:
bin/rails typed_enums:generate
Check files in CI:
bin/rails typed_enums:check
The check task exits non-zero when files are missing, changed, or stale. It does not modify files.
Watch model files and regenerate after saves:
bin/rails typed_enums:watch
This is useful when you want generated enum files to update immediately after saving a Rails model in VS Code without relying on a running Rails server.
Development Auto-Regeneration
In development, generation runs on Rails boot and through Rails.application.reloader.to_prepare when auto_generate_in_development is enabled.
When watch_models_in_development is enabled, typed_enums also watches app/models/**/*.rb and regenerates enum files after model saves. This covers normal VS Code save workflows while the Rails development process is running. If you are not running Rails, use bin/rails typed_enums:watch in a separate terminal.
The writer compares content before writing, so unchanged files are not rewritten and file watchers should stay quiet.
Production does not auto-write files by default. Run bin/rails typed_enums:generate as part of your build or release process when generated enum files are not committed.
Stale File Cleanup
typed_enums only overwrites files that it created.
If enums.js or enums.d.ts already exists without this generated marker, generation fails instead of overwriting the file:
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
Move the existing file, delete it, or configure a different config.output_dir.
The writer removes stale generated .js, .d.ts, and legacy .ts files inside the configured output directory only when they start with:
// AUTO-GENERATED BY typed_enums. DO NOT EDIT.
It does not delete user-owned files or files outside the generated directory.
Namespaced Models
Namespaced models are flattened into safe JavaScript identifiers:
Admin::Task
Generates exports inside:
app/javascript/lib/enums.js
app/javascript/lib/enums.d.ts
export const AdminTask = {};
Empty Output
If no models define enums, typed_enums still writes enums.js and enums.d.ts files with the generated header and no exports.
Troubleshooting
If models are missing, confirm they can be eager-loaded in the current environment. The scanner calls Rails.application.eager_load! before reading descendants.
If the root model class is not ApplicationRecord, configure root_model_class.
If generated files cannot be written, check filesystem permissions for config.output_dir.
If check mode fails in CI, run bin/rails typed_enums:generate locally and commit the generated files, or add generation to your build before JavaScript or TypeScript compilation.
Security
Enum values are written to frontend-visible JavaScript files. Do not put secrets, credentials, private tokens, or private application state in enum values.
Compatibility
The gem targets modern Rails applications and supports Rails 7.1 or newer. It uses Active Record enum APIs and ActiveSupport inflections, and it does not depend on any frontend framework.
Contributing
Run:
bundle exec rspec
bundle exec rubocop
Keep the gem small and focused: Active Record enums in, JavaScript constants and TypeScript declaration types out.
License
MIT.