Skip to content

Typing your rows

The SDK uses two generics:

ts
useGrist<TRow, TMapped>()
GenericMeaning
TRowThe raw shape of a row — real Grist column ids as keys.
TMappedThe logical shape — your declared column names from columns.

The id field is always present and always number. Both generics are layered on top of GristRowRecord<T> = T & { id: number }.

Declaring the shapes

ts
type RawTask = {
  Name: string
  isDone: boolean
  Priority: number
  Tags: string[]
}

type MappedTask = {
  Title: string
  Done: boolean
  Priority?: number
  Tags?: string[]
}

const w = useGrist<RawTask, MappedTask>()

w.record           // GristRowRecord<RawTask> | null
w.records          // GristRowRecord<RawTask>[] | null
w.mappedRecord     // MappedTask | null

w.table.update({
  id: w.record!.id,
  fields: w.mapBack({ Done: true }),  // mapBack typed as Partial<MappedTask>
})

Reality of "any" cells

Grist columns can be any type, including Any. The SDK can't statically prove a column's shape — your generics are an assertion. Two strategies:

  1. Trust the user / your column declarations. Fine for internal widgets where you control the document.
  2. Validate at the boundary. Run safeParseGristTableData(...) over fetched data, or hand-write Zod schemas for records you read reactively.
ts
import { z } from "zod"

const TaskSchema = z.object({
  id: z.number(),
  Name: z.string(),
  isDone: z.boolean(),
})

const parsed = TaskSchema.safeParse(w.record)
if (parsed.success) {
  // parsed.data is fully typed
}

Decoded vs encoded cells

The subscription stream and fetchSelected* return decoded values (Date instead of ["D", epoch, tz], etc.). When you type a column as Date, that's what you get.

For fetchTable (raw columnar), you'll see encoded values unless you also pass column metadata:

ts
const rows = await w.fetchTableRows<MyRow>("Tasks", {
  columns: schema.columns,  // type-aware decode
})

Without columns, Date columns return numbers, Refs return numbers, RefLists return arrays of numbers.

Records vs rows

SurfaceShape
w.record / w.recordsGristRowRecord<TRow> — fully populated row object.
w.fetchTable(...)Columnar — { [colId]: unknown[] } plus id: number[].
w.fetchTableRows(...)GristRowRecord<TRow>[] — sorted and decoded.

When you want to traverse a fetched table mostly row-wise, prefer fetchTableRows.

Strongly typed safe-parse

When you ask for safeParse: true, each cell becomes a GristSafeParseCellResult<T>:

ts
const rows = await w.fetchTableRows<MyTask>("Tasks", {
  columns: schema.columns,
  safeParse: true,
})

// rows[0].Name has shape:
//   { ok, typedValue, displayValue, raw, issues }

For a typed-array view, use a per-column projection:

ts
import { safeParseRowsToDisplayRows } from "grist-widget-sdk"
const display = safeParseRowsToDisplayRows(rows)

Component-level slicing

The SDK's slice hooks (useGristSelection<TRow, TMapped>(), useGristWrites()) carry the same generics — type once at the call site, drill into specific component subtrees as needed.

Released under the ISC License.