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"}
  ]
end

Then 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
end

Use 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: true

See 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