Define once in Elixir. Generate everywhere in TypeScript.

Typelizer is a library for Phoenix apps with a TypeScript frontend. It gives you:

  • A serializer DSL that is both the runtime serializer and the source of the TypeScript types.
  • TypeScript interfaces inferred from Ecto schemas. Ecto.Enum fields become literal unions.
  • Typed route helpers generated from your Phoenix router, with URL defaults and typed query params.
  • Envelope and pagination helpers (Envelope<T>, Paginated<T>) for JSON APIs and Inertia props.
  • Typed page props for Inertia.js, with a runtime check in dev and test.
  • mix typelizer.gen to write the files and mix typelizer.check to fail on drift in CI and in git hooks.

Installation

def deps do
  [
    {:typelizer, "~> 0.2"}
  ]
end

ecto, ecto_sql, phoenix, plug and inertia are optional dependencies. Typelizer uses them when your app has them. Typelizer supports Elixir 1.15+ and OTP 26+.

Quick start: Phoenix + Inertia + React

1. Write a serializer. It serializes at runtime and it defines the type.

defmodule MyAppWeb.TaskSerializer do
  use Typelizer.Serializer, schema: MyApp.Tasks.Task

  attributes [:id, :title, :status, :due_on]
  attribute :overdue, type: :boolean, value: &MyApp.Tasks.overdue?/1
  has_one :assignee, serializer: MyAppWeb.UserSerializer, nullable: true
end

2. Declare the props of your Inertia pages in the controller.

defmodule MyAppWeb.TaskController do
  use MyAppWeb, :controller
  use Typelizer.InertiaPage

  page "tasks/index", props: [tasks: {:list, MyAppWeb.TaskSerializer}]

  def index(conn, _params) do
    conn
    |> assign_prop(:tasks, MyAppWeb.TaskSerializer.serialize_many(MyApp.Tasks.list()))
    |> render_inertia("tasks/index")
  end
end

3. Configure and generate.

# config/config.exs
config :typelizer, repo: MyApp.Repo
config :inertia, camelize_props: true

# config/dev.exs and config/test.exs
config :typelizer, validate_inertia_props: true
# lib/my_app_web/router.ex, in the :browser pipeline, after Inertia.Plug
plug Typelizer.InertiaPage.ValidateProps
mix typelizer.gen

4. Use the types in React.

import type { TasksIndexProps } from "@/generated/pages";
import { routes } from "@/generated/routes";

export default function Index({ tasks }: TasksIndexProps) {
  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <a href={routes.task.show(task.id).url}>{task.title}</a>
          {task.status === "done" && " ✓"}
        </li>
      ))}
    </ul>
  );
}

5. Fail on drift. Run mix typelizer.check in a pre-commit hook, in CI and before a deploy. It exits with status 1 when the committed TypeScript does not match the Elixir code.

Guides

Comparison with the Ruby gem

Typelizer for Elixir is a clean-room port of the idea behind the Ruby gem typelizer by Svyatoslav Kryukov. It shares no code with it.

Ruby typelizerTypelizer for Elixir
SerializersAdds types to existing serializer libraries (Alba, ActiveModel::Serializers and others)Ships its own serializer DSL: one module serializes and defines the type
Type sourceActiveRecord models and the database schemaEcto schemas, plus PostgreSQL column nullability when a repo is configured
EnumsModel enumsEcto.Enum → literal unions
Route helpersFrom config/routes.rbFrom the Phoenix router
Inertia page propsNoYes, with a runtime check in dev and test
Drift checkYour own taskBuilt in: mix typelizer.check
RegenerationA file listener in developmentmix typelizer.gen

Roadmap

Not in v0.1:

  • Zod schemas as an output.
  • OpenAPI documents as an output.
  • Ash resources as a type source.

Credits

The idea, the name and the developer experience come from skryukov/typelizer. Thank you.

License

MIT. See LICENSE.