<!--
SPDX-FileCopyrightText: 2025 Torkild G. Kjevik
SPDX-FileCopyrightText: 2025 ash_typescript contributors <https://github.com/ash-project/ash_typescript/graphs.contributors>
SPDX-License-Identifier: MIT
-->
# Configuration Reference
This document provides a comprehensive reference for all AshTypescript configuration options.
## Application Configuration
Configure AshTypescript in your `config/config.exs` file:
```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",
  # 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 checking
```
### Configuration 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:
```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
```
### RPC 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:
```elixir
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
```
**Options:**
- `ts_result_type_name` - Name for the generated result type
- `ts_fields_const_name` - Name for the generated fields constant
- `fields` - 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
```elixir
# Default: snake_case → camelCase
# user_name → userName
# created_at → createdAt
```
### Configuration Options
```elixir
config :ash_typescript,
  input_field_formatter: :camel_case,   # How inputs are formatted
  output_field_formatter: :camel_case   # How outputs are formatted
```
**Available 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:
```elixir
# 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)
```bash
# .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:
```typescript
const response = await fetchFunction(process.env.RPC_RUN_ENDPOINT || '/rpc/run', fetchOptions);
```
#### Option 2: Vite Environment Variables
```bash
# .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:
```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:
```typescript
// 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.com
```
#### Option 4: Complex Conditional Expressions
For browser-based applications that need different endpoints based on hostname:
```elixir
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:
```typescript
// 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
}
```
```typescript
// 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:
```elixir
# 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:
```elixir
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
```typescript
// 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](../topics/lifecycle-hooks.md) 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:
```elixir
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
end
```
**Generated TypeScript:**
```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:
```elixir
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
end
```
**Generated TypeScript:**
```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:
```elixir
# 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
end
```
**Generated TypeScript:**
```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
```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;
}
```
## 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
```elixir
# config/config.exs
config :ash_typescript,
  type_mapping_overrides: [
    {AshUUID.UUID, "string"},
    {SomeComplex.Custom.Type, "CustomTypes.MyCustomType"}
  ]
```
### Example: Mapping Dependency Types
```elixir
# 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
```
```elixir
# 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"
    }
  ]
```
```typescript
// customTypes.ts - Define the MyCustomType type
export type MyCustomType = {
  someField: string;
  anotherField: number;
};
```
**Generated TypeScript:**
```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/0` callback instead
## Custom Type Imports
Import custom TypeScript modules into the generated code:
```elixir
config :ash_typescript,
  import_into_generated: [
    %{
      import_name: "CustomTypes",
      file: "./customTypes"
    },
    %{
      import_name: "MyAppConfig",
      file: "./myAppConfig"
    }
  ]
```
This generates:
```typescript
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
```elixir
# 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.Map` without `fields` constraint
- `Ash.Type.Keyword` without `fields` constraint
- `Ash.Type.Tuple` without `fields` constraint
- `Ash.Type.Struct` without `instance_of` or `fields` constraint
**Maps with field constraints are NOT affected** and will still generate typed objects.
### Type Safety Comparison
**With `Record<string, any>` (default):**
```typescript
// 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):**
```typescript
// 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
```elixir
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
end
```
**Generated TypeScript:**
```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
```elixir
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:
```typescript
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
```elixir
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:
```typescript
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](../topics/phoenix-channels.md).
## See Also
- [Getting Started Tutorial](../tutorials/getting-started.md) - Initial setup and basic usage
- [Mix Tasks Reference](mix-tasks.md) - Code generation commands
- [Phoenix Channels](../topics/phoenix-channels.md) - Channel-based RPC actions
- [Troubleshooting Reference](troubleshooting.md) - Common problems and solutions