Configuration Reference
View SourceThis document provides a comprehensive reference for all AshTypescript configuration options.
Application Configuration
Configure AshTypescript in your config/config.exs file:
# 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",
# Dynamic endpoints (for separate frontend projects)
# run_endpoint: {:runtime_expr, "CustomTypes.getRunEndpoint()"},
# validate_endpoint: {:runtime_expr, "process.env.RPC_VALIDATE_ENDPOINT"},
# Field formatting
input_field_formatter: :camel_case,
output_field_formatter: :camel_case,
# Multitenancy
require_tenant_parameters: false,
# Lifecycle hooks (optional)
# rpc_action_before_request_hook: "RpcHooks.beforeActionRequest",
# rpc_action_after_request_hook: "RpcHooks.afterActionRequest",
# rpc_validation_before_request_hook: "RpcHooks.beforeValidationRequest",
# rpc_validation_after_request_hook: "RpcHooks.afterValidationRequest",
# 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",
# Phoenix channel lifecycle hooks (optional, requires generate_phx_channel_rpc_actions: true)
# rpc_action_before_channel_push_hook: "ChannelHooks.beforeChannelPush",
# rpc_action_after_channel_response_hook: "ChannelHooks.afterChannelResponse",
# rpc_validation_before_channel_push_hook: "ChannelHooks.beforeValidationChannelPush",
# rpc_validation_after_channel_response_hook: "ChannelHooks.afterValidationChannelResponse",
# Custom type imports
import_into_generated: [
%{
import_name: "CustomTypes",
file: "./customTypes"
}
],
# Type mapping overrides for dependency types
type_mapping_overrides: [
{AshUUID.UUID, "string"},
{SomeComplex.Custom.Type, "CustomTypes.MyCustomType"}
],
# TypeScript type for untyped maps
# untyped_map_type: "Record<string, any>" # Default - allows any value type
# untyped_map_type: "Record<string, unknown>" # Stricter - requires type checkingConfiguration Options
| Option | Type | Default | Description |
|---|---|---|---|
output_file | string | "assets/js/ash_rpc.ts" | Path where generated TypeScript code will be written |
run_endpoint | string | {:runtime_expr, string} | "/rpc/run" | Endpoint for executing RPC actions |
validate_endpoint | string | {:runtime_expr, string} | "/rpc/validate" | Endpoint for validating RPC requests |
input_field_formatter | :camel_case | :snake_case | :camel_case | How to format field names in request inputs |
output_field_formatter | :camel_case | :snake_case | :camel_case | How to format field names in response outputs |
require_tenant_parameters | boolean | false | Whether to require tenant parameters in RPC calls |
rpc_action_before_request_hook | string | nil | nil | Function called before RPC action requests |
rpc_action_after_request_hook | string | nil | nil | Function called after RPC action requests |
rpc_validation_before_request_hook | string | nil | nil | Function called before validation requests |
rpc_validation_after_request_hook | string | nil | nil | Function called after validation requests |
rpc_action_hook_context_type | string | "Record<string, any>" | TypeScript type for action hook context |
rpc_validation_hook_context_type | string | "Record<string, any>" | TypeScript type for validation hook context |
generate_zod_schemas | boolean | true | Whether to generate Zod validation schemas |
zod_import_path | string | "zod" | Import path for Zod library |
zod_schema_suffix | string | "ZodSchema" | Suffix for generated Zod schema names |
generate_validation_functions | boolean | true | Whether to generate form validation functions |
generate_phx_channel_rpc_actions | boolean | false | Whether to generate Phoenix channel-based RPC functions |
phoenix_import_path | string | "phoenix" | Import path for Phoenix library |
rpc_action_before_channel_push_hook | string | nil | nil | Function called before channel push for actions |
rpc_action_after_channel_response_hook | string | nil | nil | Function called after channel response for actions |
rpc_validation_before_channel_push_hook | string | nil | nil | Function called before channel push for validations |
rpc_validation_after_channel_response_hook | string | nil | nil | Function called after channel response for validations |
rpc_action_channel_hook_context_type | string | "Record<string, any>" | TypeScript type for channel action hook context |
rpc_validation_channel_hook_context_type | string | "Record<string, any>" | TypeScript type for channel validation hook context |
import_into_generated | list | [] | List of custom modules to import |
type_mapping_overrides | list | [] | Override TypeScript types for Ash types |
untyped_map_type | string | "Record<string, any>" | TypeScript type for untyped maps |
Domain Configuration
Configure RPC actions and typed queries in your domain modules:
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
endRPC Action Configuration
Each rpc_action can be configured with:
- First argument - Name of the generated TypeScript function (e.g.,
:list_todos) - Second argument - Name of the Ash action to execute (e.g.,
:read)
Typed Query Configuration
Typed queries allow you to define pre-configured field selections with generated TypeScript types:
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]
},
]
endOptions:
ts_result_type_name- Name for the generated result typets_fields_const_name- Name for the generated fields constantfields- Pre-configured field selection array
Field Formatting
AshTypescript automatically converts field names between Elixir's snake_case convention and TypeScript's camelCase convention.
Default Behavior
# Default: snake_case → camelCase
# user_name → userName
# created_at → createdAtConfiguration Options
config :ash_typescript,
input_field_formatter: :camel_case, # How inputs are formatted
output_field_formatter: :camel_case # How outputs are formattedAvailable formatters:
:camel_case- Converts to camelCase (e.g.,user_name→userName):snake_case- Keeps snake_case (e.g.,user_name→user_name)
Dynamic RPC Endpoints
For separate frontend projects or different deployment environments, AshTypescript supports dynamic endpoint configuration through runtime TypeScript expressions.
Why Use Dynamic Endpoints?
When building a separate frontend project (not embedded in your Phoenix app), you may need different backend endpoint URLs for:
- Development:
http://localhost:4000/rpc/run - Staging:
https://staging-api.myapp.com/rpc/run - Production:
https://api.myapp.com/rpc/run
Instead of hardcoding the endpoint in your Elixir config, you can use runtime expressions that will be evaluated at runtime in your TypeScript code.
Configuration Options
You can use various runtime expressions depending on your needs:
# config/config.exs
config :ash_typescript,
# Option 1: Use environment variables directly (Node.js)
run_endpoint: {:runtime_expr, "process.env.RPC_RUN_ENDPOINT || '/rpc/run'"},
validate_endpoint: {:runtime_expr, "process.env.RPC_VALIDATE_ENDPOINT || '/rpc/validate'"},
# Option 2: Use Vite environment variables
# run_endpoint: {:runtime_expr, "import.meta.env.VITE_RPC_RUN_ENDPOINT || '/rpc/run'"},
# validate_endpoint: {:runtime_expr, "import.meta.env.VITE_RPC_VALIDATE_ENDPOINT || '/rpc/validate'"},
# Option 3: Use custom functions from imported modules
# run_endpoint: {:runtime_expr, "MyAppConfig.getRunEndpoint()"},
# validate_endpoint: {:runtime_expr, "MyAppConfig.getValidateEndpoint()"},
# Option 4: Use complex expressions with conditionals
# run_endpoint: {:runtime_expr, "window.location.hostname === 'localhost' ? 'http://localhost:4000/rpc/run' : '/rpc/run'"},
# Import modules if needed for custom functions (Option 3)
# import_into_generated: [
# %{
# import_name: "MyAppConfig",
# file: "./myAppConfig"
# }
# ]Usage Examples
Option 1: Environment Variables (Node.js/Next.js)
# .env.local
RPC_RUN_ENDPOINT=http://localhost:4000/rpc/run
RPC_VALIDATE_ENDPOINT=http://localhost:4000/rpc/validate
# .env.production
RPC_RUN_ENDPOINT=https://api.myapp.com/rpc/run
RPC_VALIDATE_ENDPOINT=https://api.myapp.com/rpc/validate
Generated TypeScript will use the environment variables directly:
const response = await fetchFunction(process.env.RPC_RUN_ENDPOINT || '/rpc/run', fetchOptions);Option 2: Vite Environment Variables
# .env.development
VITE_RPC_RUN_ENDPOINT=http://localhost:4000/rpc/run
# .env.production
VITE_RPC_RUN_ENDPOINT=https://api.myapp.com/rpc/run
Generated TypeScript:
const response = await fetchFunction(import.meta.env.VITE_RPC_RUN_ENDPOINT || '/rpc/run', fetchOptions);Option 3: Custom Functions
Create a TypeScript file with functions that return the appropriate endpoints:
// myAppConfig.ts
export function getRunEndpoint(): string {
// Use environment variables from your frontend build system
const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:4000";
return `${baseUrl}/rpc/run`;
}
export function getValidateEndpoint(): string {
const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:4000";
return `${baseUrl}/rpc/validate`;
}
// For different environments:
// Development: VITE_API_URL=http://localhost:4000
// Staging: VITE_API_URL=https://staging-api.myapp.com
// Production: VITE_API_URL=https://api.myapp.comOption 4: Complex Conditional Expressions
For browser-based applications that need different endpoints based on hostname:
config :ash_typescript,
run_endpoint: {:runtime_expr, """
(window.location.hostname === 'localhost'
? 'http://localhost:4000/rpc/run'
: `https://${window.location.hostname}/rpc/run`)
"""}This allows dynamic endpoint resolution based on the current page's hostname.
Generated Code
The generated RPC functions will use your runtime expressions directly in the code:
// Example 1: With environment variables
// config: run_endpoint: {:runtime_expr, "process.env.RPC_RUN_ENDPOINT || '/rpc/run'"}
export async function createTodo<Fields extends CreateTodoFields>(
config: CreateTodoConfig<Fields>
): Promise<CreateTodoResult<Fields>> {
// Runtime expression is embedded directly
const response = await fetchFunction(
process.env.RPC_RUN_ENDPOINT || '/rpc/run',
fetchOptions
);
// ... rest of implementation
}// Example 2: With custom function
// config: run_endpoint: {:runtime_expr, "MyAppConfig.getRunEndpoint()"}
import * as MyAppConfig from "./myAppConfig";
export async function createTodo<Fields extends CreateTodoFields>(
config: CreateTodoConfig<Fields>
): Promise<CreateTodoResult<Fields>> {
// Custom function is called at runtime
const response = await fetchFunction(
MyAppConfig.getRunEndpoint(),
fetchOptions
);
// ... rest of implementation
}Lifecycle Hooks Configuration
AshTypescript provides lifecycle hooks that allow you to inject custom logic before and after HTTP requests and Phoenix Channel pushes. These hooks enable cross-cutting concerns like authentication, logging, telemetry, performance tracking, and error monitoring.
Why Use Lifecycle Hooks?
Lifecycle hooks provide a centralized way to:
- Add authentication tokens - Inject auth headers for all requests
- Log requests and responses - Track API calls for debugging
- Measure performance - Time API calls and track latency
- Send telemetry - Report metrics to monitoring services
- Handle errors globally - Track errors in Sentry, Datadog, etc.
- Transform requests - Modify config before sending
Configuration
Configure lifecycle hooks for HTTP-based RPC actions:
# config/config.exs
config :ash_typescript,
# HTTP lifecycle hooks for RPC actions
rpc_action_before_request_hook: "RpcHooks.beforeActionRequest",
rpc_action_after_request_hook: "RpcHooks.afterActionRequest",
# HTTP lifecycle hooks for validation actions
rpc_validation_before_request_hook: "RpcHooks.beforeValidationRequest",
rpc_validation_after_request_hook: "RpcHooks.afterValidationRequest",
# TypeScript types for hook context (optional)
rpc_action_hook_context_type: "RpcHooks.ActionHookContext",
rpc_validation_hook_context_type: "RpcHooks.ValidationHookContext",
# Import the module containing your hook functions
import_into_generated: [
%{
import_name: "RpcHooks",
file: "./rpcHooks"
}
]Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
rpc_action_before_request_hook | string | nil | nil | Function called before RPC action requests |
rpc_action_after_request_hook | string | nil | nil | Function called after RPC action requests |
rpc_validation_before_request_hook | string | nil | nil | Function called before validation requests |
rpc_validation_after_request_hook | string | nil | nil | Function called after validation requests |
rpc_action_hook_context_type | string | "Record<string, any>" | TypeScript type for action hook context |
rpc_validation_hook_context_type | string | "Record<string, any>" | TypeScript type for validation hook context |
Phoenix Channel Lifecycle Hooks
For Phoenix Channel-based RPC actions, configure channel-specific hooks. Like HTTP hooks, channel hooks are separated between actions and validations:
config :ash_typescript,
# Enable channel RPC generation
generate_phx_channel_rpc_actions: true,
# Channel lifecycle hooks for RPC actions
rpc_action_before_channel_push_hook: "ChannelHooks.beforeChannelPush",
rpc_action_after_channel_response_hook: "ChannelHooks.afterChannelResponse",
# Channel lifecycle hooks for validation actions
rpc_validation_before_channel_push_hook: "ChannelHooks.beforeValidationChannelPush",
rpc_validation_after_channel_response_hook: "ChannelHooks.afterValidationChannelResponse",
# Channel hook context types (optional)
rpc_action_channel_hook_context_type: "ChannelHooks.ChannelActionHookContext",
rpc_validation_channel_hook_context_type: "ChannelHooks.ChannelValidationHookContext",
# Import the module containing channel hooks
import_into_generated: [
%{
import_name: "ChannelHooks",
file: "./channelHooks"
}
]Channel Hook Options
| Option | Type | Default | Description |
|---|---|---|---|
rpc_action_before_channel_push_hook | string | nil | nil | Function called before channel push for actions |
rpc_action_after_channel_response_hook | string | nil | nil | Function called after channel response for actions |
rpc_validation_before_channel_push_hook | string | nil | nil | Function called before channel push for validations |
rpc_validation_after_channel_response_hook | string | nil | nil | Function called after channel response for validations |
rpc_action_channel_hook_context_type | string | "Record<string, any>" | TypeScript type for channel action hook context |
rpc_validation_channel_hook_context_type | string | "Record<string, any>" | TypeScript type for channel validation hook context |
Example Hook Implementation
// rpcHooks.ts
export interface ActionHookContext {
enableLogging?: boolean;
enableTiming?: boolean;
customHeaders?: Record<string, string>;
}
export async function beforeActionRequest<T>(
action: string,
config: T & { hookCtx?: ActionHookContext }
): Promise<T & { hookCtx?: ActionHookContext }> {
const startTime = performance.now();
if (config.hookCtx?.enableLogging) {
console.log(`[${action}] Request started`, config);
}
// Add auth token
const token = localStorage.getItem('authToken');
const headers = {
...config.headers,
...config.hookCtx?.customHeaders,
...(token && { 'Authorization': `Bearer ${token}` })
};
return {
...config,
headers,
hookCtx: {
...config.hookCtx,
startTime
}
};
}
export async function afterActionRequest<T>(
action: string,
config: T & { hookCtx?: ActionHookContext },
result: any
): Promise<any> {
if (config.hookCtx?.enableTiming) {
const duration = performance.now() - (config.hookCtx as any).startTime;
console.log(`[${action}] Completed in ${duration}ms`);
}
return result;
}For complete details and examples, see the Lifecycle Hooks documentation.
Field and Argument Name Mapping
TypeScript has stricter identifier rules than Elixir. AshTypescript provides built-in verification and mapping for invalid field and argument names.
Invalid Name Patterns
AshTypescript detects and requires mapping for these patterns:
- Underscores before digits:
field_1,address_line_2,item__3 - Question marks:
is_active?,enabled?
Resource Field Mapping
Map invalid field names using the field_names option in your resource's typescript block:
defmodule MyApp.User do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypescript.Resource]
typescript do
type_name "User"
# Map invalid field names to valid TypeScript identifiers
field_names [
address_line_1: :address_line1,
address_line_2: :address_line2,
is_active?: :is_active
]
end
attributes do
attribute :name, :string, public?: true
attribute :address_line_1, :string, public?: true
attribute :address_line_2, :string, public?: true
attribute :is_active?, :boolean, public?: true
end
endGenerated TypeScript:
// Input (create/update)
const user = await createUser({
input: {
name: "John",
addressLine1: "123 Main St", // Mapped from address_line_1
addressLine2: "Apt 4B", // Mapped from address_line_2
isActive: true // Mapped from is_active?
},
fields: ["id", "name", "addressLine1", "addressLine2", "isActive"]
});
// Output - same mapped names
if (result.success) {
console.log(result.data.addressLine1); // "123 Main St"
console.log(result.data.isActive); // true
}Action Argument Mapping
Map invalid action argument names using the argument_names option:
typescript do
type_name "Todo"
argument_names [
search: [query_string_1: :query_string1],
filter_todos: [is_completed?: :is_completed]
]
end
actions do
read :search do
argument :query_string_1, :string
end
read :filter_todos do
argument :is_completed?, :boolean
end
endGenerated TypeScript:
// Arguments use mapped names
const results = await searchTodos({
input: { queryString1: "urgent tasks" }, // Mapped from query_string_1
fields: ["id", "title"]
});
const filtered = await filterTodos({
input: { isCompleted: false }, // Mapped from is_completed?
fields: ["id", "title"]
});Map Type Field Mapping
For invalid field names in map/keyword/tuple type constraints, create a custom Ash.Type.NewType with the typescript_field_names/0 callback:
# Define custom type with field mapping
defmodule MyApp.CustomMetadata do
use Ash.Type.NewType,
subtype_of: :map,
constraints: [
fields: [
field_1: [type: :string],
is_active?: [type: :boolean],
line_2: [type: :string]
]
]
@impl true
def typescript_field_names do
[
field_1: :field1,
is_active?: :isActive,
line_2: :line2
]
end
end
# Use custom type in resource
defmodule MyApp.Resource do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypescript.Resource]
typescript do
type_name "Resource"
end
attributes do
attribute :metadata, MyApp.CustomMetadata, public?: true
end
endGenerated TypeScript:
type Resource = {
metadata: {
field1: string; // Mapped from field_1
isActive: boolean; // Mapped from is_active?
line2: string; // Mapped from line_2
}
}Verification and Error Messages
AshTypescript includes three verifiers that check for invalid names at compile time:
Resource field verification error:
Invalid field names found that contain question marks, or numbers preceded by underscores.
Invalid field names in resource MyApp.User:
- attribute address_line_1 → address_line1
- attribute is_active? → is_active
You can use field_names in the typescript section to provide valid alternatives.Map constraint verification error:
Invalid field names found in map/keyword/tuple type constraints.
Invalid constraint field names in attribute :metadata on resource MyApp.Resource:
- field_1 → field1
- is_active? → is_active
To fix this, create a custom Ash.Type.NewType using map/keyword/tuple as a subtype,
and define the `typescript_field_names/0` callback to map invalid field names to valid ones.Custom Types
Create custom Ash types with TypeScript integration:
Basic Custom Type
# 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// 2. Create TypeScript type definitions in customTypes.ts
export type PriorityScore = number;
export type ColorPalette = {
primary: string;
secondary: string;
accent: string;
};# 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
endThe generated TypeScript will automatically include your custom types:
// 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;
}Type Mapping Overrides
When using custom Ash types from dependencies (where you can't add the typescript_type_name/0 callback), use the type_mapping_overrides configuration to map them to TypeScript types.
Configuration
# config/config.exs
config :ash_typescript,
type_mapping_overrides: [
{AshUUID.UUID, "string"},
{SomeComplex.Custom.Type, "CustomTypes.MyCustomType"}
]Example: Mapping Dependency Types
# Suppose you're using a third-party library with a custom type
defmodule MyApp.Product do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypescript.Resource]
typescript do
type_name "Product"
end
attributes do
uuid_primary_key :id
attribute :name, :string, public?: true
# Type from a dependency - can't modify it to add typescript_type_name
attribute :uuid, AshUUID.UUID, public?: true
attribute :some_value, SomeComplex.Custom.Type, public?: true
end
end# Configure the type mappings
config :ash_typescript,
type_mapping_overrides: [
# Map to built-in TypeScript type
{AshUUID.UUID, "string"},
# Map to custom type (requires defining the type in customTypes.ts)
{SomeComplex.Custom.Type, "CustomTypes.MyCustomType"}
],
# Import your custom types
import_into_generated: [
%{
import_name: "CustomTypes",
file: "./customTypes"
}
]// customTypes.ts - Define the MyCustomType type
export type MyCustomType = {
someField: string;
anotherField: number;
};Generated TypeScript:
import * as CustomTypes from "./customTypes";
interface ProductResourceSchema {
id: string;
name: string;
uuid: string; // Mapped to built-in string type
someValue: CustomTypes.MyCustomType; // Mapped to custom type
}When to Use Type Mapping Overrides
- ✅ Third-party Ash types from dependencies you don't control
- ✅ Library types like
AshUUID.UUID, etc. - ❌ Your own types - prefer using
typescript_type_name/0callback instead
Custom Type Imports
Import custom TypeScript modules into the generated code:
config :ash_typescript,
import_into_generated: [
%{
import_name: "CustomTypes",
file: "./customTypes"
},
%{
import_name: "MyAppConfig",
file: "./myAppConfig"
}
]This generates:
import * as CustomTypes from "./customTypes";
import * as MyAppConfig from "./myAppConfig";Import Configuration Options
| Option | Type | Description |
|---|---|---|
import_name | string | Name to use for the import (e.g., CustomTypes) |
file | string | Relative path to the module file (e.g., ./customTypes) |
Untyped Map Type Configuration
By default, AshTypescript generates Record<string, any> for map-like types without field constraints. You can configure this to use stricter types like Record<string, unknown> for better type safety.
Configuration
# config/config.exs
config :ash_typescript,
# Default - allows any value type (more permissive)
untyped_map_type: "Record<string, any>"
# Stricter - requires type checking before use (recommended for new projects)
# untyped_map_type: "Record<string, unknown>"
# Custom - use your own type definition
# untyped_map_type: "MyCustomMapType"What Gets Affected
This configuration applies to all map-like types without field constraints:
Ash.Type.MapwithoutfieldsconstraintAsh.Type.KeywordwithoutfieldsconstraintAsh.Type.TuplewithoutfieldsconstraintAsh.Type.Structwithoutinstance_oforfieldsconstraint
Maps with field constraints are NOT affected and will still generate typed objects.
Type Safety Comparison
With Record<string, any> (default):
// More permissive - values can be used directly
const todo = await getTodo({ fields: ["id", "customData"] });
if (todo.success && todo.data.customData) {
const value = todo.data.customData.someField; // OK - no error
console.log(value.toUpperCase()); // Runtime error if not a string!
}With Record<string, unknown> (stricter):
// Stricter - requires type checking before use
const todo = await getTodo({ fields: ["id", "customData"] });
if (todo.success && todo.data.customData) {
const value = todo.data.customData.someField; // Type: unknown
console.log(value.toUpperCase()); // ❌ TypeScript error!
// Must check type first
if (typeof value === 'string') {
console.log(value.toUpperCase()); // ✅ OK
}
}Example Resources
defmodule MyApp.Todo do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshTypescript.Resource]
attributes do
# Untyped map - uses configured untyped_map_type
attribute :custom_data, :map, public?: true
# Typed map - always generates typed object (not affected by config)
attribute :metadata, :map, public?: true, constraints: [
fields: [
priority: [type: :string],
tags: [type: {:array, :string}]
]
]
end
endGenerated TypeScript:
// With untyped_map_type: "Record<string, unknown>"
type TodoResourceSchema = {
customData: Record<string, unknown> | null; // Uses configured type
metadata: { // Typed object (not affected)
priority: string;
tags: Array<string>;
} | null;
}When to Use Each Option
Use Record<string, any> when:
- You need maximum flexibility
- You're working with truly dynamic data structures
- You trust your backend data and want faster development
- Backward compatibility with existing code is important
Use Record<string, unknown> when:
- You want maximum type safety
- You're starting a new project
- You want to catch potential runtime errors at compile time
- You prefer explicit type checking over implicit assumptions
Zod Schema Configuration
AshTypescript can generate Zod validation schemas for runtime type validation.
Configuration
config :ash_typescript,
# Enable/disable Zod schema generation
generate_zod_schemas: true,
# Import path for Zod library
zod_import_path: "zod",
# Suffix for generated schema names
zod_schema_suffix: "ZodSchema"Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
generate_zod_schemas | boolean | true | Whether to generate Zod validation schemas |
zod_import_path | string | "zod" | Import path for Zod library |
zod_schema_suffix | string | "ZodSchema" | Suffix appended to schema names |
Generated Output
When enabled, generates schemas like:
import { z } from "zod";
export const TodoZodSchema = z.object({
id: z.string(),
title: z.string(),
completed: z.boolean().nullable()
});Phoenix Channel Configuration
AshTypescript can generate Phoenix channel-based RPC functions alongside HTTP-based functions.
Configuration
config :ash_typescript,
# Enable Phoenix channel RPC action generation
generate_phx_channel_rpc_actions: true,
# Import path for Phoenix library
phoenix_import_path: "phoenix"Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
generate_phx_channel_rpc_actions | boolean | false | Whether to generate channel-based RPC functions |
phoenix_import_path | string | "phoenix" | Import path for Phoenix library |
Generated Output
When enabled, generates both HTTP and channel-based functions:
import { Channel } from "phoenix";
// HTTP-based (always available)
export async function listTodos<Fields extends ListTodosFields>(
config: ListTodosConfig<Fields>
): Promise<ListTodosResult<Fields>> {
// ... HTTP implementation
}
// Channel-based (when enabled)
export function listTodosChannel<Fields extends ListTodosFields>(
config: ListTodosChannelConfig<Fields>
): void {
// ... Channel implementation
}For more details on using Phoenix channels, see the Phoenix Channels topic documentation.
See Also
- Getting Started Tutorial - Initial setup and basic usage
- Mix Tasks Reference - Code generation commands
- Phoenix Channels - Channel-based RPC actions
- Troubleshooting Reference - Common problems and solutions