TanStack / TanStack/db

Feature: Compile-time field guards for collection CRUD operations

Open
#1,450 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.9k
Forks
266
Avg merge
1d 4h
Merged PRs (30d)
55

Description

Problem

When certain fields on a collection should only be updated through specific optimistic actions (e.g., a status field that triggers server-side effects via createOptimisticAction), there's currently no way to prevent direct collection.update() calls from modifying those fields at the type level.

For example, given a collection like:

interface Order {
  id: string
  name: string
  status: "pending" | "approved" | "rejected"
}

const orders = createCollection<Order>({
  id: "orders",
  // ...
})

// This is the intended way to update status — it calls an API with side effects
const updateStatus = createOptimisticAction({
  onMutate: ({ id, status }) => {
    orders.update(id, (draft) => { draft.status = status })
  },
  mutationFn: async ({ id, status }) => {
    await api.updateOrderStatus(id, status) // triggers side effects
  },
})

// But nothing prevents someone from doing this directly:
orders.update("order-1", (draft) => {
  draft.status = "approved" // ← bypasses the API, no side effects
})

Today the only option is a runtime guard (e.g., checking field names in a mutation handler or proxy wrapper), but there's no compile-time safety. This is especially problematic in larger teams where conventions are easy to miss.

Proposed Solution

Introduce a mechanism to mark certain fields as "guarded" or "readonly" at the collection level, so that collection.update(), collection.insert(), and collection.delete() type-check against a restricted subset of the type.

A few possible API shapes (not mutually exclusive):

Option A: guardedFields config option
const orders = createCollection<Order>({
  id: "orders",
  guardedFields: ["status"] as const,
  // ...
})

// Type error: Property 'status' does not exist on type WritableDeep<Omit<Order, "status">>
orders.update("order-1", (draft) => {
  draft.status = "approved" // ← compile error
})

// Optimistic actions could bypass the guard via a flag or separate internal type
const updateStatus = createOptimisticAction({
  onMutate: ({ id, status }) => {
    orders.update(id, { bypassGuard: true }, (draft) => {
      draft.status = status // ← allowed
    })
  },
  // ...
})
Option B: Generic type parameter for update input

Similar to how TInsertInput already allows the insert type to differ from TOutput, introduce a TUpdateInput parameter:

type OrderUpdatable = Omit<Order, "status">

const orders = createCollection<Order, string, {}, typeof schema, Order, OrderUpdatable>({
  id: "orders",
  // ...
})

// draft is WritableDeep<OrderUpdatable> — no 'status' property
orders.update("order-1", (draft) => {
  draft.status = "approved" // ← compile error
})
Option C: Readonly<> marker on the type itself

Leverage TypeScript's readonly modifier — fields marked readonly on the type would be excluded from WritableDeep in the update callback:

interface Order {
  id: string
  name: string
  readonly status: "pending" | "approved" | "rejected" // ← not writable via update()
}

This is the least invasive but reuses readonly semantics in a potentially confusing way since readonly in TS doesn't normally mean "can never be set."

Current Workaround

Runtime validation in a mutation handler or custom wrapper around the collection that strips guarded fields. This works but doesn't surface errors at development time.

Additional Context

The TInsertInput generic already establishes the pattern of having different input types for different operations. Extending this pattern to updates feels natural. The proxy-based WritableDeep<TInput> draft in update() callbacks is the main type surface that would need to be narrowed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the createCollection and update type surfaces, especially TInsertInput and WritableDeep, and review how createOptimisticAction currently interacts with collection updates. Compare the guardedFields, TUpdateInput, and readonly proposals with the existing API, then define the chosen contract and verify that guarded fields are rejected for direct CRUD operations while remaining usable through the approved path.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
databases, developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.