Typing your rows
The SDK uses two generics:
useGrist<TRow, TMapped>()| Generic | Meaning |
|---|---|
TRow | The raw shape of a row — real Grist column ids as keys. |
TMapped | The 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
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:
- Trust the user / your column declarations. Fine for internal widgets where you control the document.
- Validate at the boundary. Run
safeParseGristTableData(...)over fetched data, or hand-write Zod schemas for records you read reactively.
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:
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
| Surface | Shape |
|---|---|
w.record / w.records | GristRowRecord<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>:
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:
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.