From 355e2bd9fe4bdae0082359954b8560e491c89916 Mon Sep 17 00:00:00 2001 From: Arham Amin <132888838+arhxam@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:35:34 +0530 Subject: [PATCH] fix(table-core): preserve undefined in DeepValue for optional deep keys When a deep string accessor key traverses an optional (or nullable) parent key, `DeepValue` dropped the `undefined` from the resolved value type. This made `getValue()` for a path like `user.salary.amount` typed as `number` even when `salary` is optional, hiding a possible `undefined` that the runtime accessor (which uses optional chaining) can return. Distribute over the leaf union and map `null`/`undefined` members to `undefined`, matching the optional-chaining semantics of the deep accessor function. --- .changeset/deep-value-optional-key.md | 5 ++++ packages/table-core/src/types/type-utils.ts | 5 ++-- .../tests/unit/helpers/columnHelper.test.ts | 29 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 .changeset/deep-value-optional-key.md diff --git a/.changeset/deep-value-optional-key.md b/.changeset/deep-value-optional-key.md new file mode 100644 index 0000000000..cb0b7c6c09 --- /dev/null +++ b/.changeset/deep-value-optional-key.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +Preserve `undefined` in `DeepValue` accessor value types when a deep string accessor key traverses an optional or nullable parent key. `getValue()` for a path like `user.salary.amount` is now typed as `number | undefined` when `salary` is optional, matching the optional-chaining behavior of the runtime deep accessor. diff --git a/packages/table-core/src/types/type-utils.ts b/packages/table-core/src/types/type-utils.ts index cf6be1c3d6..fef6e1263a 100644 --- a/packages/table-core/src/types/type-utils.ts +++ b/packages/table-core/src/types/type-utils.ts @@ -76,8 +76,9 @@ type DeepKeysPrefix< ? `${TPrefix}.${DeepKeys & string}` : never -export type DeepValue = - T extends Record +export type DeepValue = T extends null | undefined + ? undefined + : T extends Record ? TProp extends `${infer TBranch}.${infer TDeepProp}` ? DeepValue : T[TProp & keyof T] diff --git a/packages/table-core/tests/unit/helpers/columnHelper.test.ts b/packages/table-core/tests/unit/helpers/columnHelper.test.ts index 2389434ff7..ec7de5d61a 100644 --- a/packages/table-core/tests/unit/helpers/columnHelper.test.ts +++ b/packages/table-core/tests/unit/helpers/columnHelper.test.ts @@ -99,4 +99,33 @@ describe('createColumnHelper', () => { expect(row.getValue('0')).toBe('Alice') expect(row.getValue('1')).toBe(42) }) + + it('should preserve undefined for optional deep accessor keys', () => { + type DeepPerson = { + user: { + salary?: { + amount: number + } + } + } + const deepHelper = createColumnHelper() + const column = deepHelper.accessor('user.salary.amount', { + cell: (info) => { + // `salary` is optional, so the resolved deep value can be `undefined` + expectTypeOf(info.getValue()).toEqualTypeOf() + return info.getValue() + }, + }) + const table = constructTable({ + features, + columns: deepHelper.columns([column]), + data: [{ user: { salary: { amount: 42 } } }], + }) + const row = table.getRowModel().rows[0]! + + expect(table.getAllLeafColumns().map((c) => c.id)).toEqual([ + 'user_salary_amount', + ]) + expect(row.getValue('user_salary_amount')).toBe(42) + }) })