Skip to content

Support generic record types across the data + grid layer (#3691)#4458

Draft
amcclain wants to merge 17 commits into
developfrom
data-layer-generics
Draft

Support generic record types across the data + grid layer (#3691)#4458
amcclain wants to merge 17 commits into
developfrom
data-layer-generics

Conversation

@amcclain

Copy link
Copy Markdown
Member

Closes #3691.

Adds opt-in generic type parameters <T, Id> across the data and grid layer, so apps can declare a record's data shape once and get typed access wherever records surface.

What you get

  • new Store<Person>({...}) / new GridModel<Person>({...}) → typed store.records[i].data, gridModel.selectedRecord.data, and record.id.
  • record.id is typed Id (auto-derived from the interface's id via RecordId<T>, or pass it explicitly: Store<Trade, string>), so ids flow into typed service APIs without casts.
  • Parameterized: Store, StoreRecord, StoreSelectionModel, GridModel, DataViewModel, ZoneGridModel, UrlStore, RestStore, RestGridModel.

Design

  • Fully opt-in / zero-breakage: every param defaults (T = PlainObject, Id = RecordId<T>), so existing untyped code compiles and behaves exactly as before.
  • record.data is typed as the bare field shape T - consistent with Hoist's "id is not a field" model. id is the top-level record.id, not a member of data.
  • T appears in output positions only (the write API stays PlainObject), keeping Store<Person> covariantly assignable to plain Store - so all internal framework consumers compile unchanged.
  • Internal impl (RecordSet, validators) stays monomorphic; the public read getters cast at the boundary.

Demos

  • Migrated admin RoleModel (GridModel<RoleGridRow>, drops record.data as HoistRole casts on the selection path) and ClusterObjectsModel (GridModel<ClusterObjectRecord>).
  • A companion Toolbox PR migrates the Portfolio / Todo / Recalls examples.

Not in scope (deliberate)

  • Column renderer/editor callbacks and RecordAction callbacks still receive an untyped StoreRecord - typed callback context is a possible future follow-on. The demo migrations honestly retain casts there.

Verification

  • A compile-time type-assertion fixture (data/__typetests__/dataGenerics.typetest.ts) guards typed data, derived/explicit id, the zero-breakage cast, and covariance.
  • tsc --noEmit and yarn lint clean; docs (data + grid READMEs, CHANGELOG, doc registry) updated.

amcclain added 16 commits June 25, 2026 11:33
Also update RoleModel.ts data casts (as unknown as HoistRole) to preserve
zero-error baseline - these casts worked when data was plain PlainObject/any
but now data is typed as PlainObject & {id: StoreRecordId} which TypeScript's
overlap check rejects. The double-cast is a bridge until Task 8 migrates
RoleModel to Store<HoistRole>.
Fixture lives in data/__typetests__/ and is picked up by the existing
tsconfig include pattern (./**/*). Since tsconfig uses emitDeclarationOnly,
no runtime artifact is produced. No tsconfig exclude addition needed.
Added eslint-disable for no-unused-vars since all variables exist solely
for compile-time type assertions.
…mpatibility

Change StoreRecord.data and committedData from T & {id: Id} to T & {id?: Id}.
The required id extension caused PlainObject & {id: StoreRecordId} for untyped
stores, which TypeScript's TS2352 overlap check rejected when casting to concrete
interfaces (e.g. `record.data as HoistRole`). Making id optional eliminates the
concrete named property conflict while preserving full structural covariance.

Also exports RecordData<T, Id> = T & {id?: Id} as a utility type alias, adds a
zero-breakage cast guard to the fixture, and reverts four as-unknown-as-HoistRole
workarounds in RoleModel back to as-HoistRole.

Note: the prescribed conditional-type RecordData (string extends keyof T) was
investigated but caused TypeScript to treat T as invariant in class bodies,
producing 20 errors in Store.ts/StoreSelectionModel.ts. The optional-id approach
achieves the same goal without touching those files.
Per design decision: data reflects exactly the declared field shape, consistent
with Hoist's 'id is not a field' model. record.id carries the typed Id; data.id
is available only when the data interface itself declares it. Keeps Store<T>
covariantly assignable to Store and leaves untyped-store casts working unchanged.
…ta casts

- Add local `RoleGridRow extends HoistRole` to cover synthetic fields
  (`isGroupRow`, `effectiveUserNames`, etc.) added by `processRawData`
  and `processRolesForTreeGrid`
- Type `gridModel` property and `createGridModel()` as `GridModel<RoleGridRow>`
- Remove `as HoistRole` cast in `selectedRole` getter - now typed via generic
- Remove `as HoistRole` cast in `onRowDoubleClicked` - `record` is `any` there
- Retain casts in action callbacks (ActionFnData.record is untyped StoreRecord)
- CHANGELOG.md: New Features entry for opt-in Store/StoreRecord/GridModel/etc. generic <T, Id> params
- data/README.md: Add "Typing Records" subsection under StoreRecord covering interface definition,
  id derivation (auto vs. explicit), getValues() return type, opt-in behavior, and current limitation
  on column renderer/RecordAction callbacks
- data/StoreRecord.ts: JSDoc note on `data` property clarifying id is stamped at runtime but use
  record.id for type-safe access
- data/__typetests__/dataGenerics.typetest.ts: Add getValues() type assertion (returns T & {id: Id})
- Add a 'Typed Records' section to cmp/grid/README.md showing GridModel<T> typed
  selectedRecord/store.records/record.id, with the renderer/RecordAction untyped-callback
  caveat and a cross-link to the data package's Typing Records section.
- Add a forward pointer to Typing Records from the StoreRecord intro in data/README.md.
- Add 'generics'/'typed records'/'RecordId' discoverability keywords to the data and grid
  doc-registry entries.
The release-prep merge from develop renamed the prior SNAPSHOT to released 86.2.0 and
opened 87.0.0-SNAPSHOT; relocate the generics New Features bullet to the new topmost
unreleased heading.
@amcclain

Copy link
Copy Markdown
Member Author

Companion Toolbox PR exercising these generic data-layer types in the example apps: xh/toolbox#874

@amcclain amcclain requested review from haynesjm42 and lbwexler June 25, 2026 21:53
Use descriptive, T-prefixed type-parameter names across the data + grid generics
(Store, StoreRecord, StoreSelectionModel, GridModel, DataViewModel, ZoneGridModel,
UrlStore, RestStore, RestGridModel, and their *Config interfaces + the RecordId helper)
instead of the ambiguous bare <T, Id>. TData reads clearly as the record's data shape
and aligns with ag-Grid's own TData convention at the column-callback boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Store generic types to spec types for StoreRecord.data and StoreRecord.id

1 participant