Field Selection

View Source

Field selection is a core concept in AshTypescript that lets you precisely specify which data you need from your Ash resources.

Why Field Selection?

  • Reduced payload size - Only requested fields are returned
  • Better performance - Ash only loads and processes requested data
  • Full type safety - TypeScript infers exact return types based on selected fields
  • Explicit data requirements - No over-fetching or under-fetching

Basic Field Selection

Simple Fields

import { getTodo } from './ash_rpc';

const todo = await getTodo({
  fields: ["id", "title", "completed", "priority"],
  input: { id: "todo-123" }
});

if (todo.success) {
  // TypeScript knows exact shape:
  // { id: string, title: string, completed: boolean, priority: string }
  console.log(todo.data.title);
}

Note: There is no "select all" option. This is intentional to prevent over-fetching and ensure explicit data requirements for full type-safety.

Nested Field Selection

Relationships

Select fields from related resources:

const todo = await getTodo({
  fields: [
    "id",
    "title",
    { user: ["name", "email", "avatarUrl"] }
  ],
  input: { id: "todo-123" }
});

if (todo.success) {
  console.log("Todo:", todo.data.title);
  console.log("Created by:", todo.data.user.name);
}

Multiple Relationships

const todo = await getTodo({
  fields: [
    "id",
    "title",
    {
      user: ["name", "email"],
      assignee: ["name", "email"],
      tags: ["name", "color"]
    }
  ],
  input: { id: "todo-123" }
});

Deep Nesting

Select fields from nested relationships:

const todo = await getTodo({
  fields: [
    "id",
    "title",
    {
      comments: [
        "id",
        "text",
        {
          author: [
            "name",
            { profile: ["bio", "avatarUrl"] }
          ]
        }
      ]
    }
  ],
  input: { id: "todo-123" }
});

Calculations

Basic Calculations

Request calculated fields computed by your Ash resource:

const todo = await getTodo({
  fields: [
    "id",
    "title",
    "completionPercentage",  // Calculated field
    "timeRemaining"          // Calculated field
  ],
  input: { id: "todo-123" }
});

Calculations Returning Complex Types

For calculations returning complex types (unions, embedded resources) without arguments, use nested syntax:

const todo = await getTodo({
  fields: [
    "id",
    {
      relatedItem: ["article", { article: ["id", "title"] }]
    }
  ],
  input: { id: "todo-123" }
});

Calculations with Arguments

Pass arguments to calculation fields:

const todo = await getTodo({
  fields: [
    "id",
    {
      priorityScore: {
        args: { multiplier: 2.5, includeSubtasks: true },
        fields: ["score", "rank", "category"]
      }
    }
  ],
  input: { id: "todo-123" }
});

Embedded Resources

Basic Embedded Resources

const todo = await getTodo({
  fields: [
    "id",
    { settings: ["theme", "notifications", "timezone"] }
  ],
  input: { id: "todo-123" }
});

Embedded Arrays

const todo = await getTodo({
  fields: [
    "id",
    { attachments: ["filename", "size", "url"] }
  ],
  input: { id: "todo-123" }
});

if (todo.success) {
  todo.data.attachments.forEach(attachment => {
    console.log(`${attachment.filename} (${attachment.size} bytes)`);
  });
}

Union Types

For union type fields, select fields from specific union members:

const todo = await getTodo({
  fields: [
    "id",
    {
      content: [
        "text",
        {
          textContent: ["text", "formatting"],
          imageContent: ["url", "caption"],
          videoContent: ["url", "thumbnail"]
        }
      ]
    }
  ],
  input: { id: "todo-123" }
});

See Union Types for detailed union type handling.

Load Restrictions

Actions can restrict which relationships/calculations clients can load using allowed_loads or denied_loads:

// If action has: allowed_loads: [:user]
const result = await listTodosLimited({
  fields: ["id", "title", { user: ["name"] }]  // OK
  // { comments: ["text"] }  // Would fail - not in allowed_loads
});

See RPC Action Options for configuring load restrictions.

Common Patterns

List vs Detail Views

// List view: minimal fields
async function fetchTodoList() {
  return await listTodos({
    fields: ["id", "title", "completed", "priority"]
  });
}

// Detail view: full fields with relationships
async function fetchTodoDetail(id: string) {
  return await getTodo({
    fields: [
      "id", "title", "description", "completed", "priority",
      "dueDate", "createdAt", "updatedAt",
      {
        user: ["name", "email", "avatarUrl"],
        comments: ["id", "text", { author: ["name"] }],
        tags: ["name", "color"]
      }
    ],
    input: { id }
  });
}

Reusable Field Definitions

const TodoFields = {
  basic: ["id", "title", "completed"] as const,

  withUser: [
    "id", "title", "completed",
    { user: ["name", "email"] }
  ] as const,

  full: [
    "id", "title", "description", "completed", "priority",
    {
      user: ["name", "email", "avatarUrl"],
      tags: ["name", "color"]
    }
  ] as const
};

// Usage
const todos = await listTodos({ fields: TodoFields.withUser });

Conditional Field Selection

type ViewMode = "list" | "grid" | "detail";

function getTodoFields(mode: ViewMode) {
  const baseFields = ["id", "title", "completed"];

  switch (mode) {
    case "list":
      return [...baseFields, { user: ["name"] }];
    case "grid":
      return [...baseFields, "priority", { tags: ["color"] }];
    case "detail":
      return [
        ...baseFields,
        "description", "priority", "dueDate",
        {
          user: ["name", "email"],
          comments: ["id", "text", { author: ["name"] }]
        }
      ];
  }
}

Next Steps