Field and Argument Name Mapping

View Source

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: "addressLine1",
      address_line_2: "addressLine2",
      is_active?: "isActive"
    ]
  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:

// 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: "queryString1"],
    filter_todos: [is_completed?: "isCompleted"]
  ]
end

actions do
  read :search do
    argument :query_string_1, :string
  end

  read :filter_todos do
    argument :is_completed?, :boolean
  end
end

Generated 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]
      ]
    ]

  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:

type Resource = {
  metadata: {
    field1: string;      // Mapped from field_1
    isActive: boolean;   // Mapped from is_active?
    line2: string;       // Mapped from line_2
  }
}

Metadata Field Name Mapping

For invalid metadata field names, use the metadata_field_names option on the RPC action:

defmodule MyApp.Domain do
  use Ash.Domain, extensions: [AshTypescript.Rpc]

  typescript_rpc do
    resource MyApp.Task do
      rpc_action :read_with_metadata, :read_with_metadata,
        show_metadata: [:field_1, :is_cached?, :metric_2],
        metadata_field_names: [
          field_1: "field1",
          is_cached?: "isCached",
          metric_2: "metric2"
        ]
    end
  end
end

Generated TypeScript:

const tasks = await readWithMetadata({
  fields: ["id", "title"],
  metadataFields: ["field1", "isCached", "metric2"]  // Mapped names
});

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.

Metadata Field Verification Error

Invalid metadata field names found in show_metadata configuration.

Invalid metadata field name in RPC action:
  - RPC action: read_with_metadata (action: read)
  - Field: field_1
  - Suggested: field1
  - Reason: Contains question marks or numbers preceded by underscores

Metadata field names must be valid TypeScript identifiers and cannot conflict with resource fields.

Automatic Field Formatting

By default, AshTypescript converts field names between Elixir's snake_case and TypeScript's camelCase:

# Elixir (snake_case)
:user_name  "userName"
:created_at  "createdAt"

Configuration

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 (default)
  • :snake_case - Keeps snake_case

Next Steps