Skip to content

fix: correct generateDynamicNodeCallbacks return type - #2633

Open
camielvs wants to merge 1 commit into
masterfrom
fix/dynamic-node-callbacks-return-type
Open

fix: correct generateDynamicNodeCallbacks return type#2633
camielvs wants to merge 1 commit into
masterfrom
fix/dynamic-node-callbacks-return-type

Conversation

@camielvs

Copy link
Copy Markdown
Collaborator

Part of B3 of #2626.

The bug

generateDynamicNodeCallbacks binds the node & task ids into each NodeCallbacks entry, so what it returns takes one fewer argument than what it received. It declared its return type as NodeCallbacks anyway — the un-bound shape — so every callback it handed back was typed as still needing an ids argument that the wrapper already supplies.

Nothing caught this because the only caller, createTaskNode, launders the result through as Node.

The fix

Return TaskNodeCallbacks, the shape actually produced. Once the types line up, the reflective Object.fromEntries construction is no longer needed to satisfy the compiler, so the object is built explicitly. That removes:

  • the ExcludeNodeAndTaskId helper type
  • an ...args: any[] spread
  • all three as casts

and turns a missing or misordered argument into a compile error instead of something the casts absorb.

Behaviour deltas

Two, both verified unreachable in production:

Delta Why it can't bite
No-callbacks branch returns DEFAULT_TASK_NODE_CALLBACKS instead of {} as NodeCallbacks, so consumers gating on callback truthiness (TaskDetails/Actions.tsx:64,74,90) now see no-ops rather than undefined FlowCanvas.tsx:394-404 always passes nodeCallbacks; the branch is never taken
The ids object is built once and shared by all seven wrappers rather than rebuilt per call All seven useNodeCallbacks implementations only read ids.taskId/ids.nodeId; none mutates it

TaskNodeCallbacks is exported for the annotation. CallbackWithIds is un-exported — it's used only inside src/types/taskNode.ts, and Knip fails CI on an export consumed solely within its own file.

Tests

Adds generateDynamicNodeCallbacks.test.ts (6 tests) — the wrapper had no coverage. It pins id derivation, id injection ahead of caller args, undefined trailing args, laziness, and the no-callbacks branch.

Reverting only the type change makes the new test file fail to compile with 8 arity errors, so the test genuinely depends on the fix rather than merely accompanying it.

Verification

typecheck · lint · knip · prettier clean; full suite 192 files / 1972 tests passing.

Scope is v1-only: NodeCallbacks appears in 8 files, all under the v1 canvas. v2's single edge to this area is a type-only TaskNodeContextType import.

Left deliberately out of scope: TaskNodeProvider.tsx:51 declares a third, structurally different local TaskNodeCallbacks (adds setCollapsed, makes four callbacks optional). Collapsing it is a separate change.

🤖 Generated with Claude Code

`generateDynamicNodeCallbacks` wraps each `NodeCallbacks` entry so the
node & task ids are bound in, which means the values it returns take one
fewer argument than the ones it received. It nonetheless declared its
return type as `NodeCallbacks` — the un-bound shape — so every returned
callback was typed as needing an ids argument it in fact supplies itself.
The mismatch was invisible only because the sole caller
(`createTaskNode`) casts its result through `as Node`.

Return `TaskNodeCallbacks` instead, which is the shape actually
produced. With the types lined up, the reflective `Object.fromEntries`
construction is no longer needed to satisfy the compiler, so build the
object explicitly: this drops the `ExcludeNodeAndTaskId` helper, an
`...args: any[]` spread, and all three casts, and it makes a missing or
misordered argument a compile error rather than something the casts
absorb.

Two side effects, both verified unreachable in production:

- the no-callbacks branch now returns `DEFAULT_TASK_NODE_CALLBACKS`
  rather than `{} as NodeCallbacks`, so consumers that gate on callback
  truthiness (`TaskDetails/Actions.tsx`) see no-ops instead of
  `undefined`. `FlowCanvas` always passes `nodeCallbacks`, so the branch
  is not taken.
- the ids object is created once and shared by all seven wrappers
  instead of being rebuilt per call. Every `useNodeCallbacks`
  implementation only reads `ids.taskId`/`ids.nodeId`; none mutates it.

`TaskNodeCallbacks` is exported for the annotation, and
`CallbackWithIds` is un-exported — it is used only within
`src/types/taskNode.ts`, and Knip fails on an export used solely in its
own file.

Adds unit coverage for the wrapper, which had none. Reverting the type
change alone makes the new test file fail to compile with 8 arity
errors.

Part of B3 of #2626.
@camielvs
camielvs requested a review from a team as a code owner August 14, 2026 20:25
@github-actions

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: fix/dynamic-node-callbacks-return-type/b55c868

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.

1 participant