# AshTypescript
**๐ฅ Automatic TypeScript type generation for Ash resources and actions**
Generate type-safe TypeScript clients directly from your Elixir Ash resources, ensuring end-to-end type safety between your backend and frontend. Never write API types manually again.
[](https://hex.pm/packages/ash_typescript)
[](https://hexdocs.pm/ash_typescript)
[](LICENSE)
## โก Quick Start
**Get up and running in under 5 minutes:**
### 1. Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:ash_typescript, "~> 0.2.0"}
]
end
```
### 2. Configure your domain
```elixir
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Todo do
rpc_action :list_todos, :read
rpc_action :create_todo, :create
rpc_action :get_todo, :get
end
end
resources do
resource MyApp.Todo
end
end
```
### 3. Set up Phoenix RPC controller
```elixir
defmodule MyAppWeb.RpcController do
use MyAppWeb, :controller
def run(conn, params) do
# Actor (and tenant if needed) must be set on the conn before calling run/2 or validate/2
# If your pipeline does not set these, you must add something like the following code:
# conn = Ash.PlugHelpers.set_actor(conn, conn.assigns[:current_user])
# conn = Ash.PlugHelpers.set_tenant(conn, conn.assigns[:tenant])
result = AshTypescript.Rpc.run_action(:my_app, conn, params)
json(conn, result)
end
def validate(conn, params) do
result = AshTypescript.Rpc.validate_action(:my_app, conn, params)
json(conn, result)
end
end
```
### 4. Add RPC routes
Add these routes to your `router.ex` to map the RPC endpoints:
```elixir
scope "/rpc", MyAppWeb do
pipe_through :api # or :browser if using session-based auth
post "/run", RpcController, :run
post "/validate", RpcController, :validate
end
```
### 5. Generate TypeScript types
**Recommended approach** (runs codegen for all Ash extensions in your project):
```bash
mix ash.codegen --dev"
```
**Alternative approach** (runs codegen only for AshTypescript):
```bash
mix ash_typescript.codegen --output "assets/js/ash_rpc.ts"
```
### 6. Use in your frontend
```typescript
import { listTodos, createTodo } from './ash_rpc';
// โ
Fully type-safe API calls
const todos = await listTodos({
fields: ["id", "title", "completed"],
filter: { completed: false }
});
const newTodo = await createTodo({
fields: ["id", "title", { user: ["name", "email"] }],
input: { title: "Learn AshTypescript", priority: "high" }
});
```
**๐ That's it!** Your TypeScript frontend now has compile-time type safety for your Elixir backend.
## ๐ Features
- **๐ฅ Zero-config TypeScript generation** - Automatically generates types from Ash resources
- **๐ก๏ธ End-to-end type safety** - Catch integration errors at compile time, not runtime
- **โก Smart field selection** - Request only needed fields with full type inference
- **๐ฏ RPC client generation** - Type-safe function calls for all action types
- **๐ก Phoenix Channel support** - Generate channel-based RPC functions for real-time applications
- **๐ข Multitenancy ready** - Automatic tenant parameter handling
- **๐ฆ Advanced type support** - Enums, unions, embedded resources, and calculations
- **๐ง Highly configurable** - Custom endpoints, formatting, and output options
- **๐งช Runtime validation** - Zod schemas for runtime type checking and form validation
- **๐ Auto-generated filters** - Type-safe filtering with comprehensive operator support
- **๐ Form validation** - Client-side validation functions for all actions
- **๐ฏ Typed queries** - Pre-configured queries for SSR and optimized data fetching
- **๐จ Flexible field formatting** - Separate input/output formatters (camelCase, snake_case, etc.)
- **๐ Custom HTTP clients** - Support for custom fetch functions and request options (axios, interceptors, etc.)
## ๐ Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Usage Examples](#usage-examples)
- [Advanced Features](#advanced-features)
- [Configuration](#configuration)
- [Mix Tasks](#mix-tasks)
- [API Reference](#api-reference)
- [Requirements](#requirements)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [License](#license)
## ๐๏ธ Core Concepts
### How it works
1. **Resource Definition**: Define your Ash resources with attributes, relationships, and actions
2. **RPC Configuration**: Expose specific actions through your domain's RPC configuration
3. **Type Generation**: Run `mix ash_typescript.codegen` to generate TypeScript types
4. **Frontend Integration**: Import and use fully type-safe client functions
### Type Safety Benefits
- **Compile-time validation** - TypeScript compiler catches API misuse
- **Autocomplete support** - Full IntelliSense for all resource fields and actions
- **Refactoring safety** - Rename fields in Elixir, get TypeScript errors immediately
- **Documentation** - Generated types serve as living API documentation
## ๐ก Usage Examples
### Basic CRUD Operations
```typescript
import { listTodos, getTodo, createTodo, updateTodo, destroyTodo } from './ash_rpc';
// List todos with field selection
const todos = await listTodos({
fields: ["id", "title", "completed", "priority"],
filter: { status: "active" },
sort: "-priority,+createdAt"
});
// Get single todo with relationships
const todo = await getTodo({
fields: ["id", "title", { user: ["name", "email"] }],
id: "todo-123"
});
// Create new todo
const newTodo = await createTodo({
fields: ["id", "title", "createdAt"],
input: {
title: "Learn AshTypescript",
priority: "high",
dueDate: "2024-01-01"
}
});
// Update existing todo (primary key separate from input)
const updatedTodo = await updateTodo({
fields: ["id", "title", "priority", "updatedAt"],
primaryKey: "todo-123", // Primary key as separate parameter
input: {
title: "Updated: Learn AshTypescript",
priority: "urgent"
}
});
// Delete todo (primary key separate from input)
const deletedTodo = await destroyTodo({
fields: [],
primaryKey: "todo-123" // Primary key as separate parameter
});
```
### Advanced Field Selection
```typescript
// Complex nested field selection
const todoWithDetails = await getTodo({
fields: [
"id", "title", "description",
{
user: ["name", "email", "avatarUrl"],
comments: ["id", "text", { author: ["name"] }],
tags: ["name", "color"]
}
],
id: "todo-123"
});
// Calculations with arguments
const todoWithCalc = await getTodo({
fields: [
"id", "title",
{
"priorityScore": {
"args": { "multiplier": 2 },
"fields": ["score", "rank"]
}
}
],
id: "todo-123"
});
```
### Error Handling
All generated RPC functions return a `{success: true/false}` structure instead of throwing exceptions:
```typescript
const result = await createTodo({
fields: ["id", "title"],
input: { title: "New Todo" }
});
if (result.success) {
// Access the created todo
console.log("Created todo:", result.data);
const todoId: string = result.data.id;
const todoTitle: string = result.data.title;
} else {
// Handle validation errors, network errors, etc.
result.errors.forEach(error => {
console.error(`Error: ${error.message}`);
if (error.fieldPath) {
console.error(`Field: ${error.fieldPath}`);
}
});
}
```
### Custom Headers and Authentication
```typescript
import { listTodos, buildCSRFHeaders } from './ash_rpc';
// With CSRF protection
const todos = await listTodos({
fields: ["id", "title"],
headers: buildCSRFHeaders()
});
// With custom authentication
const todos = await listTodos({
fields: ["id", "title"],
headers: {
"Authorization": "Bearer your-token-here",
"X-Custom-Header": "value"
}
});
```
### Custom Fetch Functions and Request Options
AshTypescript allows you to customize the HTTP client used for requests by providing custom fetch functions and additional fetch options.
#### Using fetchOptions for Request Customization
All generated RPC functions accept an optional `fetchOptions` parameter that allows you to customize the underlying fetch request:
```typescript
import { createTodo, listTodos } from './ash_rpc';
// Add request timeout and custom cache settings
const todo = await createTodo({
fields: ["id", "title"],
input: { title: "New Todo" },
fetchOptions: {
signal: AbortSignal.timeout(5000), // 5 second timeout
cache: 'no-cache',
credentials: 'include'
}
});
// Use with abort controller for cancellable requests
const controller = new AbortController();
const todos = await listTodos({
fields: ["id", "title"],
fetchOptions: {
signal: controller.signal
}
});
// Cancel the request if needed
controller.abort();
```
#### Custom Fetch Functions
You can replace the native fetch function entirely by providing a `customFetch` parameter. This is useful for:
- Adding global authentication
- Using alternative HTTP clients like axios
- Adding request/response interceptors
- Custom error handling
```typescript
// Custom fetch with user preferences and tracking
const enhancedFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
// Get user preferences from localStorage (safe, non-sensitive data)
const userLanguage = localStorage.getItem('userLanguage') || 'en';
const userTimezone = localStorage.getItem('userTimezone') || 'UTC';
const apiVersion = localStorage.getItem('preferredApiVersion') || 'v1';
// Generate correlation ID for request tracking
const correlationId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const customHeaders = {
'Accept-Language': userLanguage,
'X-User-Timezone': userTimezone,
'X-API-Version': apiVersion,
'X-Correlation-ID': correlationId,
};
return fetch(url, {
...init,
headers: {
...init?.headers,
...customHeaders
}
});
};
// Use custom fetch function
const todos = await listTodos({
fields: ["id", "title"],
customFetch: enhancedFetch
});
```
#### Using Axios with AshTypescript
While AshTypescript uses the fetch API by default, you can create an adapter to use axios or other HTTP clients:
```typescript
import axios from 'axios';
// Create axios adapter that matches fetch API
const axiosAdapter = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
try {
const url = typeof input === 'string' ? input : input.toString();
const axiosResponse = await axios({
url,
method: init?.method || 'GET',
headers: init?.headers,
data: init?.body,
timeout: 10000,
// Add other axios-specific options
validateStatus: () => true // Don't throw on HTTP errors
});
// Convert axios response to fetch Response
return new Response(JSON.stringify(axiosResponse.data), {
status: axiosResponse.status,
statusText: axiosResponse.statusText,
headers: new Headers(axiosResponse.headers as any)
});
} catch (error) {
if (error.response) {
// HTTP error status
return new Response(JSON.stringify(error.response.data), {
status: error.response.status,
statusText: error.response.statusText
});
}
throw error; // Network error
}
};
// Use axios for all requests
const todos = await listTodos({
fields: ["id", "title"],
customFetch: axiosAdapter
});
```
### Phoenix Channel-based RPC Actions
AshTypescript can generate Phoenix channel-based RPC functions alongside the standard HTTP-based functions. This is useful for real-time applications that need to communicate over WebSocket connections.
#### Configuration
Enable channel function generation in your configuration:
```elixir
# config/config.exs
config :ash_typescript,
generate_phx_channel_rpc_actions: true,
phoenix_import_path: "phoenix" # customize if needed
```
#### Generated Channel Functions
When enabled, AshTypescript generates channel functions with the suffix `Channel` for each RPC action:
```typescript
import { Channel } from "phoenix";
import { createTodo, createTodoChannel } from './ash_rpc';
// Standard HTTP-based function (always available)
const httpResult = await createTodo({
fields: ["id", "title"],
input: { title: "New Todo" }
});
// Channel-based function (generated when enabled)
createTodoChannel({
channel: myChannel,
fields: ["id", "title"],
input: { title: "New Todo" },
resultHandler: (result) => {
if (result.success) {
console.log("Todo created:", result.data);
} else {
console.error("Creation failed:", result.errors);
}
},
errorHandler: (error) => {
console.error("Channel error:", error);
},
timeoutHandler: () => {
console.error("Request timed out");
}
});
```
#### Setting up Phoenix Channels
First, establish a Phoenix channel connection:
```typescript
import { Socket } from "phoenix";
const socket = new Socket("/socket", {
params: { authToken: "your-auth-token" }
});
socket.connect();
const ashTypeScriptRpcChannel = socket.channel("ash_typescript_rpc:<user-id or something else unique>", {});
ashTypeScriptRpcChannel.join()
.receive("ok", () => console.log("Connected to channel"))
.receive("error", resp => console.error("Unable to join", resp));
```
#### Backend Channel Setup
To enable Phoenix Channel support for AshTypescript RPC actions, configure your Phoenix socket and channel handlers:
```elixir
# In your my_app_web/channels/user_socket.ex or equivalent
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
channel "ash_typescript_rpc:*", MyAppWeb.AshTypescriptRpcChannel
@impl true
def connect(params, socket, _connect_info) do
# AshTypescript assumes that socket.assigns.ash_actor & socket.assigns.ash_tenant are correctly set if needed.
# This should be done during the socket connection setup, usually by decrypting the auth token sent by the client, or any other necessary data.
# See https://hexdocs.pm/phoenix/channels.html#using-token-authentication for more information.
{:ok, socket}
end
def id(socket), do: socket.assigns.ash_actor.id
end
# In your my_app_web/channels/ash_typescript_rpc_channel.ex
defmodule MyAppWeb.AshTypescriptRpcChannel do
use Phoenix.Channel
@impl true
def join("ash_typescript_rpc:" <> _user_id, _payload, socket) do
{:ok, socket}
end
def handle_in("run", params, socket) do
result =
AshTypescript.Rpc.run_action(
:my_app,
socket,
params
)
{:reply, {:ok, result}, socket}
end
def handle_in("validate", params, socket) do
result =
AshTypescript.Rpc.validate_action(
:my_app,
socket,
params
)
{:reply, {:ok, result}, socket}
end
# Catch-all for unhandled messages
@impl true
def handle_in(event, payload, socket) do
{:reply, {:error, %{reason: "Unknown event: #{event}", payload: payload}}, socket}
end
end
```
**Important Notes:**
- Replace `:my_app` with your actual app's OTP application name (the atom used in `AshTypescript.Rpc.run_action/3`)
- The socket connection should set `socket.assigns.ash_actor` and `socket.assigns.ash_tenant` if your app uses authentication or multitenancy
#### Channel Function Features
Channel functions support all the same features as HTTP functions:
```typescript
// Pagination with channels
listTodosChannel({
channel: ashTypeScriptRpcChannel,
fields: ["id", "title", { user: ["name"] }],
filter: { status: "active" },
page: { limit: 10, offset: 0 },
resultHandler: (result) => {
if (result.success) {
console.log("Todos:", result.data.results);
console.log("Has more:", result.data.hasMore);
}
}
});
// Complex field selection
getTodoChannel({
channel: ashTypeScriptRpcChannel,
primaryKey: "todo-123",
fields: [
"id", "title", "description",
{
user: ["name", "email"],
comments: ["text", { author: ["name"] }]
}
],
resultHandler: (result) => {
// Fully type-safe result handling
}
});
```
#### Error Handling
Channel functions provide the same error structure as HTTP functions:
```typescript
createTodoChannel({
channel: myChannel,
fields: ["id", "title"],
input: { title: "New Todo" },
resultHandler: (result) => {
if (result.success) {
// result.data is fully typed based on selected fields
console.log("Created:", result.data.title);
} else {
// Handle validation errors, network errors, etc.
result.errors.forEach(error => {
console.error(`Error: ${error.message}`);
if (error.fieldPath) {
console.error(`Field: ${error.fieldPath}`);
}
});
}
},
errorHandler: (error) => {
// Handle channel-level errors
console.error("Channel communication error:", error);
},
timeoutHandler: () => {
// Handle timeouts
console.error("Request timed out");
}
});
```
### Advanced Filtering and Pagination
```typescript
import { listTodos } from './ash_rpc';
// Complex filtering with pagination
const result = await listTodos({
fields: ["id", "title", "priority", "dueDate", { user: ["name"] }],
filter: {
and: [
{ status: { eq: "ongoing" } },
{ priority: { in: ["high", "urgent"] } },
{
or: [
{ dueDate: { lessThan: "2024-12-31" } },
{ user: { name: { eq: "John Doe" } } }
]
}
]
},
sort: "-priority,+dueDate",
page: {
limit: 20,
offset: 0,
count: true
}
});
if (result.success) {
console.log(`Found ${result.data.count} todos`);
console.log(`Showing ${result.data.results.length} results`);
console.log(`Has more: ${result.data.hasMore}`);
}
```
## ๐ง Advanced Features
### Embedded Resources
Full support for embedded resources with type safety:
```elixir
# In your resource
attribute :metadata, MyApp.TodoMetadata do
public? true
end
```
```typescript
// TypeScript usage
const todo = await getTodo({
fields: [
"id", "title",
{ metadata: ["priority", "tags", "customFields"] }
],
id: "todo-123"
});
```
### Union Types
Support for Ash union types with selective field access:
```elixir
# In your resource
attribute :content, :union do
constraints types: [
text: [type: :string],
checklist: [type: MyApp.ChecklistContent]
]
end
```
```typescript
// TypeScript usage with union field selection
const todo = await getTodo({
fields: [
"id", "title",
{ content: ["text", { checklist: ["items", "completedCount"] }] }
],
id: "todo-123"
});
```
### Multitenancy Support
Automatic tenant parameter handling for multitenant resources:
```elixir
# Configuration
config :ash_typescript, require_tenant_parameters: true
```
```typescript
// Tenant parameters automatically added to function signatures
const todos = await listTodos({
fields: ["id", "title"],
tenant: "org-123"
});
```
### Calculations and Aggregates
Full support for Ash calculations with type inference:
```elixir
# In your resource
calculations do
calculate :full_name, :string do
expr(first_name <> " " <> last_name)
end
end
```
```typescript
// TypeScript usage
const users = await listUsers({
fields: ["id", "firstName", "lastName", "fullName"]
});
```
## ๐ Advanced Features
### Zod Runtime Validation
AshTypescript generates Zod schemas for all your actions, enabling runtime type checking and form validation.
#### Enable Zod Generation
```elixir
# config/config.exs
config :ash_typescript,
generate_zod_schemas: true,
zod_import_path: "zod", # or "@hookform/resolvers/zod" etc.
zod_schema_suffix: "ZodSchema"
```
#### Generated Zod Schemas
For each action, AshTypescript generates validation schemas:
#### Zod Schema Examples
```typescript
// Generated schema for creating a todo
export const createTodoZodSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high", "urgent"]).optional(),
dueDate: z.date().optional(),
tags: z.array(z.string()).optional()
});
```
### Form Validation Functions
AshTypescript generates dedicated validation functions for client-side form validation when `generate_validation_functions` is enabled:
```typescript
import { validateCreateTodo } from './ash_rpc';
// Validate form input before submission
const validationResult = await validateCreateTodo({
input: {
title: "New Todo",
priority: "high"
}
});
if (!validationResult.success) {
// Handle validation errors
validationResult.errors.forEach(error => {
console.log(`Field ${error.fieldPath}: ${error.message}`);
});
}
```
#### Channel-Based Validation
When both `generate_validation_functions` and `generate_phx_channel_rpc_actions` are enabled, AshTypescript also generates channel-based validation functions:
```typescript
import { validateCreateTodoChannel } from './ash_rpc';
import { Channel } from "phoenix";
// Validate over Phoenix channels
validateCreateTodoChannel({
channel: myChannel,
input: {
title: "New Todo",
priority: "high"
},
resultHandler: (result) => {
if (result.success) {
console.log("Validation passed");
} else {
result.errors.forEach(error => {
console.log(`Field ${error.fieldPath}: ${error.message}`);
});
}
},
errorHandler: (error) => console.error("Channel error:", error),
timeoutHandler: () => console.error("Validation timeout")
});
```
### Type-Safe Filtering
AshTypescript automatically generates comprehensive filter types for all resources:
```typescript
import { listTodos } from './ash_rpc';
// Complex filtering with full type safety
const todos = await listTodos({
fields: ["id", "title", "status", "priority"],
filter: {
and: [
{ status: { eq: "ongoing" } },
{ priority: { in: ["high", "urgent"] } },
{
or: [
{ dueDate: { lessThan: "2024-12-31" } },
{ isOverdue: { eq: true } }
]
}
]
},
sort: "-priority,+dueDate"
});
```
#### Available Filter Operators
- **Equality**: `eq`, `notEq`, `in`
- **Comparison**: `greaterThan`, `greaterThanOrEqual`, `lessThan`, `lessThanOrEqual`
- **Logic**: `and`, `or`, `not`
- **Relationships**: Nested filtering on related resources
### Typed Queries for SSR
Define reusable, type-safe queries for server-side rendering and optimized data fetching:
#### Define Typed Queries
```elixir
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Todo do
# Regular RPC actions
rpc_action :list_todos, :read
# Typed query with predefined fields
typed_query :dashboard_todos, :read do
ts_result_type_name "DashboardTodosResult"
ts_fields_const_name "dashboardTodosFields"
fields [
:id, :title, :priority, :isOverdue,
%{
user: [:name, :email],
comments: [:id, :content]
}
]
end
end
end
end
```
#### Generated TypeScript Types
```typescript
// Generated type for the typed query result
export type DashboardTodosResult = Array<InferResult<TodoResourceSchema,
["id", "title", "priority", "isOverdue",
{
user: ["name", "email"],
comments: ["id", "content"]
}]
>>;
// Reusable field constant for client-side refetching
export const dashboardTodosFields = [
"id", "title", "priority", "isOverdue",
{
user: ["name", "email"],
comments: ["id", "content"]
}
] as const;
```
#### Server-Side Usage
```elixir
# In your Phoenix controller
defmodule MyAppWeb.DashboardController do
use MyAppWeb, :controller
def index(conn, _params) do
result = AshTypescript.Rpc.run_typed_query(:my_app, :dashboard_todos, %{}, conn)
case result do
%{"success" => true, "data" => todos} ->
render(conn, "index.html", todos: todos)
%{"success" => false, "errors" => errors} ->
conn
|> put_status(:bad_request)
|> render("error.html", errors: errors)
end
end
end
```
#### Client-Side Refetching
```typescript
// Use the same field selection for client-side updates
const refreshedTodos = await listTodos({
fields: dashboardTodosFields,
filter: { isOverdue: { eq: true } }
});
```
### Flexible Field Formatting
Configure separate formatters for input parsing and output generation:
```elixir
# config/config.exs
config :ash_typescript,
# How client field names are converted to internal Elixir fields (default is :camel_case)
input_field_formatter: :camel_case,
# How internal Elixir fields are formatted for client consumption (default is :camel_case)
output_field_formatter: :camel_case
```
#### Available Formatters
- `:camel_case` - `user_name` โ `userName`
- `:pascal_case` - `user_name` โ `UserName`
- `:snake_case` - `user_name` โ `user_name`
- Custom formatter: `{MyModule, :format_field}` or `{MyModule, :format_field, [extra_args]}`
#### Different Input/Output Formatting
```elixir
# Use different formatting for input vs output
config :ash_typescript,
input_field_formatter: :snake_case, # Client sends snake_case
output_field_formatter: :camel_case # Client receives camelCase
```
## โ๏ธ Configuration
### Application Configuration
```elixir
# config/config.exs
config :ash_typescript,
# File generation
output_file: "assets/js/ash_rpc.ts",
# RPC endpoints
run_endpoint: "/rpc/run",
validate_endpoint: "/rpc/validate",
# Field formatting
input_field_formatter: :camel_case,
output_field_formatter: :camel_case,
# Multitenancy
require_tenant_parameters: false,
# Zod schema generation
generate_zod_schemas: true,
zod_import_path: "zod",
zod_schema_suffix: "ZodSchema",
# Validation functions
generate_validation_functions: true,
# Phoenix channel-based RPC actions
generate_phx_channel_rpc_actions: false,
phoenix_import_path: "phoenix",
# Custom type imports
import_into_generated: [
%{
import_name: "CustomTypes",
file: "./customTypes"
}
]
```
### Domain Configuration
```elixir
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Todo do
# Standard CRUD actions
rpc_action :list_todos, :read
rpc_action :get_todo, :get
rpc_action :create_todo, :create
rpc_action :update_todo, :update
rpc_action :destroy_todo, :destroy
# Custom actions
rpc_action :complete_todo, :complete
rpc_action :archive_todo, :archive
# Typed queries for SSR and optimized data fetching
typed_query :dashboard_todos, :read do
ts_result_type_name "DashboardTodosResult"
ts_fields_const_name "dashboardTodosFields"
fields [
:id, :title, :priority, :status,
%{
user: [:name, :email],
comments: [:id, :content]
},
]
end
end
resource MyApp.User do
rpc_action :list_users, :read
rpc_action :get_user, :get
end
end
end
```
### Field Formatting
Customize how field names are formatted in generated TypeScript:
```elixir
# Default: snake_case โ camelCase
# user_name โ userName
# created_at โ createdAt
```
### Custom Types
Create custom Ash types with TypeScript integration:
```elixir
# 1. Create custom type in Elixir
defmodule MyApp.PriorityScore do
use Ash.Type
def storage_type(_), do: :integer
def cast_input(value, _) when is_integer(value) and value >= 1 and value <= 100, do: {:ok, value}
def cast_input(_, _), do: {:error, "must be integer 1-100"}
def cast_stored(value, _), do: {:ok, value}
def dump_to_native(value, _), do: {:ok, value}
def apply_constraints(value, _), do: {:ok, value}
# AshTypescript integration
def typescript_type_name, do: "CustomTypes.PriorityScore"
end
```
```typescript
// 2. Create TypeScript type definitions in customTypes.ts
export type PriorityScore = number;
export type ColorPalette = {
primary: string;
secondary: string;
accent: string;
};
```
```elixir
# 3. Use in your resources
defmodule MyApp.Todo do
use Ash.Resource, domain: MyApp.Domain
attributes do
uuid_primary_key :id
attribute :title, :string, public?: true
attribute :priority_score, MyApp.PriorityScore, public?: true
end
end
```
The generated TypeScript will automatically include your custom types:
```typescript
// Generated TypeScript includes imports
import * as CustomTypes from "./customTypes";
// Your resource types use the custom types
interface TodoFieldsSchema {
id: string;
title: string;
priorityScore?: CustomTypes.PriorityScore | null;
}
```
## ๐ ๏ธ Mix Tasks
### Code Generation Commands
#### `mix ash.codegen` (Recommended)
**Preferred approach** for generating TypeScript types along with other Ash extensions in your project.
```bash
# Generate types for all Ash extensions including AshTypescript
mix ash.codegen --dev
# With custom output location
mix ash.codegen --dev --output "assets/js/ash_rpc.ts"
```
**When to use**: When you have multiple Ash extensions (AshPostgres, etc.) and want to run codegen for all of them together. This is the recommended approach for most projects.
#### `mix ash_typescript.codegen` (Specific)
Generate TypeScript types, RPC clients, Zod schemas, and validation functions **only for AshTypescript**.
**When to use**: When you want to run codegen specifically for AshTypescript only in your project.
**Options:**
- `--output` - Output file path (default: `assets/js/ash_rpc.ts`)
- `--run_endpoint` - RPC run endpoint (default: `/rpc/run`)
- `--validate_endpoint` - RPC validate endpoint (default: `/rpc/validate`)
- `--check` - Check if generated code is up to date (useful for CI)
- `--dry_run` - Print generated code without writing to file
**Generated Content:**
- TypeScript interfaces for all resources
- RPC client functions for each action
- Filter input types for type-safe querying
- Zod validation schemas (if enabled)
- Form validation functions
- Typed query constants and types
- Custom type imports
**Examples:**
```bash
# Basic generation (AshTypescript only)
mix ash_typescript.codegen
# Custom output location
mix ash_typescript.codegen --output "frontend/src/api/ash.ts"
# Custom RPC endpoints
mix ash_typescript.codegen \
--run_endpoint "/api/rpc/run" \
--validate_endpoint "/api/rpc/validate"
# Check if generated code is up to date (CI usage)
mix ash_typescript.codegen --check
# Preview generated code without writing to file
mix ash_typescript.codegen --dry_run
```
## ๐ API Reference
### Generated Code Structure
AshTypescript generates:
1. **TypeScript interfaces** for all resources with metadata for field selection
2. **RPC client functions** for each exposed action
3. **Validation functions** for client-side form validation
4. **Filter input types** for type-safe querying with comprehensive operators
5. **Zod schemas** for runtime validation (when enabled)
6. **Typed query constants** and result types for SSR
7. **Field selection types** for type-safe field specification
8. **Custom type imports** for external TypeScript definitions
9. **Enum types** for Ash enum types
10. **Utility functions** for headers and CSRF protection
### Generated Functions
For each `rpc_action` in your domain, AshTypescript generates:
```typescript
// For rpc_action :list_todos, :read
function listTodos<Fields extends ListTodosFields>(params: {
fields: Fields;
filter?: TodoFilterInput;
sort?: string;
page?: PaginationOptions;
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ListTodosResult<Fields>>;
// Validation function for list_todos
function validateListTodos(params: {
input: ListTodosInput;
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ValidateListTodosResult>;
// For rpc_action :create_todo, :create
function createTodo<Fields extends CreateTodosFields>(params: {
fields: Fields;
input: CreateTodoInput;
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<CreateTodoResult<Fields>>;
// Validation function for create_todo
function validateCreateTodo(params: {
input: CreateTodoInput;
headers?: Record<string, string>;
fetchOptions?: RequestInit;
customFetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}): Promise<ValidateCreateTodoResult>;
// Zod schemas (when enabled)
export const createTodoZodSchema: z.ZodObject<...>;
export const listTodosZodSchema: z.ZodObject<...>;
```
### Utility Functions
```typescript
// CSRF protection for Phoenix applications
function getPhoenixCSRFToken(): string | null;
function buildCSRFHeaders(): Record<string, string>;
```
## ๐ Requirements
- **Elixir** ~> 1.15
- **Ash** ~> 3.5
- **AshPhoenix** ~> 2.0 (for RPC endpoints)
## ๐ Troubleshooting
### Common Issues
**TypeScript compilation errors:**
- Ensure generated types are up to date: `mix ash_typescript.codegen`
- Check that all referenced resources are properly configured
**RPC endpoint errors:**
- Verify AshPhoenix RPC endpoints are configured in your router
- Check that actions are properly exposed in domain RPC configuration
**Type inference issues:**
- Ensure all attributes are marked as `public? true`
- Check that relationships are properly defined
### Debug Commands
```bash
# Check generated output without writing
mix ash_typescript.codegen --dry_run
# Validate TypeScript compilation
cd assets/js && npx tsc --noEmit
# Check for updates
mix ash_typescript.codegen --check
```
## ๐ค Contributing
### Development Setup
```bash
# Clone the repository
git clone https://github.com/ash-project/ash_typescript.git
cd ash_typescript
# Install dependencies
mix deps.get
# Run tests
mix test
# Generate test types
mix test.codegen
```
## ๐ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Support
- **Documentation**: [hexdocs.pm/ash_typescript](https://hexdocs.pm/ash_typescript)
- **Issues**: [GitHub Issues](https://github.com/ash-project/ash_typescript/issues)
- **Discussions**: [GitHub Discussions](https://github.com/ash-project/ash_typescript/discussions)
- **Ash Community**: [Ash Framework Discord](https://discord.gg/ash-framework)
---
**Built with โค๏ธ by the Ash Framework team**
*Generate once, type everywhere. Make your Elixir-TypeScript integration bulletproof.*