Skip to content

[2.x] Make NodePath non-distributive over node unions - #1205

Draft
lorisleiva wants to merge 1 commit into
10-02-use_the_published_v1_node_types_in_codama_upgradefrom
10-05-make_nodepath_non-distributive_over_node_unions
Draft

lorisleiva wants to merge 1 commit into
10-02-use_the_published_v1_node_types_in_codama_upgradefrom
10-05-make_nodepath_non-distributive_over_node_unions

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

This PR stops NodePath from splitting node unions into one path type per member:

NodePath<AccountNode | PdaNode>
// before: readonly [...Node[], AccountNode] | readonly [...Node[], PdaNode]
// after:  readonly [...Node[], AccountNode | PdaNode]

The new type is strictly wider, so existing paths still type-check. It also accepts paths built from a node only known as a union, such as [...stack.getPath(), node], which previously didn't compile against NodePath<Node>. The one thing it no longer does is fit a parameter typed as an explicit union of paths, e.g. NodePath<AccountNode> | NodePath<PdaNode>. Such parameters should use NodePath<AccountNode | PdaNode> instead.

The conditional still distributes over the node type to tell NodePath (any list of nodes) apart from NodePath<T>, but every path now ends at the whole node type. A plain [TNode] extends [undefined] check would have left NodePath<T> unresolved for generic node types, e.g. in LinkableDictionary.

undefined is excluded from the last node, so NodePath<PdaNode | undefined> remains readonly Node[] | readonly [...Node[], PdaNode].

A new NodePath.test.ts covers union, single-kind, untyped and optional paths. Its union assertion fails to compile with the previous type.

@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4f94500

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva lorisleiva changed the title Make NodePath non-distributive over node unions [2.x] Make NodePath non-distributive over node unions Oct 5, 2026
@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Makes NodePath<TNode> non-distributive over the node type: NodePath<A | B> is now a single readonly [...Node[], A | B] tuple instead of NodePath<A> | NodePath<B>. The trick is the NodePathImpl<TCheck, TNode> indirection: the conditional still distributes over TCheck (so undefined members still resolve to readonly Node[] and generic TNodes stay deferred rather than collapsing), but the tail element reads from the second, non-checked parameter, which TS does not substitute per-member. Exclude<TNode, undefined> is needed precisely because TNode is the whole union in that branch. Adds a type-level test file.

I walked through the four cases (undefined, single kind, union, X | undefined) plus the generic TLinkNode path in LinkableDictionary and the predicate narrowing in recordPath (isNodePath(linkablePath, 'accountNode') now narrows a single tuple type to a subtype rather than filtering a union — assignable, so it still works). The implementation is correct and the doc comments explain the non-obvious parts well.

A nice side effect worth knowing about: NodePath<Node> used to expand to a ~70-member union of tuple types; it's now one tuple. That should help both compile times and error messages wherever isFilledNodePath / NodePath<Node> show up.

Before merging

  • Changeset. CONTRIBUTING.md asks for a changeset on any user-facing change, and this changes a public exported type with a consumer-visible caveat (parameters typed NodePath<A> | NodePath<B> no longer accept a NodePath<A | B> argument). A patch for @codama/visitors-core (the fixed core group handles the rest) with a one-liner mirroring the PR description seems right. If the 2.x branch policy deliberately skips changesets for type-only changes, ignore this.

Notes for other reviewers

  • The only possible regression is the one named in the description: any parameter typed as an explicit union of paths. Nothing in visitors-core does that, and pnpm test:types across the monorepo is the real guard here. It's worth a quick grep for NodePath<\w+> \| in the downstream renderers (renderers-js, renderers-rust, renderers-core) before the next release, since a stack.getPath(['accountNode', 'definedTypeNode']) result would now be rejected by such a parameter.
  • Inference of TNode in getLastNodeFromPath / NodeStack.visitPath now goes through Exclude<TNode, undefined> instead of a bare TNode in the tuple tail. The test on getLastNodeFromPath(path) returning AccountNode | PdaNode confirms inference still resolves through it.

Comment thread packages/visitors-core/test/NodePath.test.ts Outdated
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.

2 participants