Summary
A column's cell render gets the row as EntityAccessor<object>, and there is no typed way to read the value at the column's own field ref from it. Every custom column that reads a relation or a second field of the row therefore casts the accessor to a hand-written shape and indexes it by name. bindx does the same internally (accessField in packages/bindx-dataview/src/columnTypes.ts returns unknown).
Environment
@contember/bindx-dataview@0.1.52
contember/bindx@main as of 78c1712
Reproduction
A custom column that shows whether a has-one is connected, declared inside a grid as <IsConnectedColumn field={it.author} />:
export const IsConnectedColumn = Object.assign(
(_props: { field: unknown }): null => null,
{
staticRender: ({ field }: { field: unknown }): ReactElement => {
const fieldName = extractFieldName(field) ?? ''
return (
<ColumnLeaf
fieldName={null}
fieldRef={null}
sortingField={null}
filterName={null}
filterHandler={undefined}
isTextSearchable={false}
renderCell={row =>
// row: EntityAccessor<object> — nothing links it to `field`
(row as unknown as Record<string, { $isConnected?: boolean } | undefined>)[fieldName]?.$isConnected
? 'yes'
: 'no'}
/>
)
},
},
)
The same happens for:
- a column that reads the items of a has-many (
row[fieldName].items);
- a column that reads the value of a scalar other than the one it binds, e.g. an enum column whose cell also shows a date from the same row: the grid's
children closure has the typed ref (it.state.publishedAt), but the cell only gets the erased row.
Expected behavior
The code that holds a typed ref (it.author, it.state.publishedAt) can read that ref's value on a given row without a cast, with the type the ref already carries.
Proposal
A resolver typed by the ref, exported from @contember/bindx-dataview (or @contember/bindx):
/** The accessor at `ref`'s path on `row`, typed as the ref. */
function resolveOnRow<TRef extends FieldRef<unknown> | HasOneRef<any> | HasManyRef<any>>(
row: EntityAccessor<object>,
ref: TRef,
): TRef
It walks FIELD_REF_META.fullPath (the same path extractFieldName and accessField use) through the row's proxy. Since the collector refs and the runtime accessors already share one type (FieldRef<T> & FieldAccessor<T>), returning TRef needs no new types. accessField and the built-in column types could then use it internally.
A wider alternative is to make ColumnLeafProps.renderCell generic over the grid's entity so the row itself is typed; that touches every column factory and the analyzer, so the resolver looks like the smaller step.
Open question for the maintainers: whether the resolver should throw or return null when the path is not in the row's selection (today a missing field raises UnfetchedFieldError on read, which seems right to keep).
Summary
A column's cell render gets the row as
EntityAccessor<object>, and there is no typed way to read the value at the column's own field ref from it. Every custom column that reads a relation or a second field of the row therefore casts the accessor to a hand-written shape and indexes it by name. bindx does the same internally (accessFieldinpackages/bindx-dataview/src/columnTypes.tsreturnsunknown).Environment
@contember/bindx-dataview@0.1.52contember/bindx@mainas of78c1712Reproduction
A custom column that shows whether a has-one is connected, declared inside a grid as
<IsConnectedColumn field={it.author} />:The same happens for:
row[fieldName].items);childrenclosure has the typed ref (it.state.publishedAt), but the cell only gets the erased row.Expected behavior
The code that holds a typed ref (
it.author,it.state.publishedAt) can read that ref's value on a given row without a cast, with the type the ref already carries.Proposal
A resolver typed by the ref, exported from
@contember/bindx-dataview(or@contember/bindx):It walks
FIELD_REF_META.fullPath(the same pathextractFieldNameandaccessFielduse) through the row's proxy. Since the collector refs and the runtime accessors already share one type (FieldRef<T> & FieldAccessor<T>), returningTRefneeds no new types.accessFieldand the built-in column types could then use it internally.A wider alternative is to make
ColumnLeafProps.renderCellgeneric over the grid's entity so the row itself is typed; that touches every column factory and the analyzer, so the resolver looks like the smaller step.Open question for the maintainers: whether the resolver should throw or return
nullwhen the path is not in the row's selection (today a missing field raisesUnfetchedFieldErroron read, which seems right to keep).