Typelizer makes your Elixir code the single source of the shapes that your TypeScript frontend sees. You define a serializer once. It serializes your data at runtime and it is the source of the TypeScript interface. Typelizer also generates typed route helpers from your Phoenix router and typed page props for Inertia.js pages.
This guide takes a Phoenix app from zero to generated types.
1. Install
Add typelizer to your dependencies in mix.exs:
def deps do
[
{:typelizer, "~> 0.2"}
]
endThen run mix deps.get.
ecto, ecto_sql, phoenix, plug and inertia are optional dependencies.
Typelizer uses them when your app has them.
2. Write a serializer
defmodule MyAppWeb.TaskSerializer do
use Typelizer.Serializer, schema: MyApp.Tasks.Task
attributes [:id, :title, :status, :due_on, :inserted_at]
attribute :overdue, type: :boolean, value: &MyApp.Tasks.overdue?/1
has_one :assignee, serializer: MyAppWeb.UserSerializer, nullable: true
has_many :comments, serializer: MyAppWeb.CommentSerializer
endUse it in a controller:
json(conn, %{data: MyAppWeb.TaskSerializer.serialize(task)})The result is a map with camelCase string keys and JSON-ready values:
%{
"id" => "0b6c5e1a-…",
"title" => "Ship v0.1",
"status" => "todo",
"dueOn" => "2026-10-01",
"insertedAt" => "2026-09-30T10:00:00Z",
"overdue" => false,
"assignee" => %{"id" => "…", "name" => "Ana"},
"comments" => []
}See Serializers for the full DSL.
3. Configure the output
All keys are optional. This is a typical setup for a Phoenix + Inertia + React app:
# config/config.exs
config :typelizer,
output: [
serializers: "assets/js/generated/serializers",
routes: "assets/js/generated/routes",
pages: "assets/js/generated/pages"
],
repo: MyApp.Repo
# config/dev.exs and config/test.exs
config :typelizer, validate_inertia_props: trueSee Configuration for every key.
4. Generate the TypeScript
mix typelizer.gen
typelizer: wrote 14 files, 0 unchanged, removed 0 stale files.The serializer above becomes assets/js/generated/serializers/Task.ts:
// Generated by typelizer — do not edit.
// Source: MyAppWeb.TaskSerializer
import type { Comment } from "./Comment";
import type { User } from "./User";
export interface Task {
id: string;
title: string;
status: "todo" | "doing" | "done";
dueOn: string | null;
insertedAt: string;
overdue: boolean;
assignee: User | null;
comments: Comment[];
}Commit the generated files. They are the contract between the backend and the frontend.
5. Use the types in React
import type { Task } from "@/generated/serializers";
import { routes } from "@/generated/routes";
export function TaskLink({ task }: { task: Task }) {
return <a href={routes.task.show(task.id).url}>{task.title}</a>;
}With Inertia, declare the props of each page and type the page component with the generated props interface. See Inertia page props.
import type { TasksIndexProps } from "@/generated/pages";
export default function Index({ tasks, filters }: TasksIndexProps) {
// …
}6. Fail on drift
mix typelizer.check
The task generates everything in memory and compares it with the files on disk. When a serializer, a schema, a route or a page declaration changed and the committed TypeScript did not follow, it prints the difference and exits with status 1:
typelizer: the generated TypeScript is out of date (2 files):
changed assets/js/generated/serializers/Task.ts
- dueOn: string | null;
+ dueOn: string;
missing assets/js/generated/routes/taskComment.ts
Run `mix typelizer.gen` and commit the result.Run it in a pre-commit hook, in a pre-push hook, in CI and before a deploy:
# .githooks/pre-commit
#!/bin/sh
mix typelizer.check
When repo: is configured, both tasks read column nullability from the
database, so the database must be up.
Next steps
- Serializers: the DSL, type inference from Ecto, nullability.
- Route helpers: typed URLs from the Phoenix router.
- Inertia page props: typed page props and runtime validation.
- Configuration: every
config :typelizerkey.