Skip to content

Preserve fixedSize for single-field struct codecs - #1971

Merged
mcintyre94 merged 2 commits into
anza-xyz:mainfrom
shin4141:fix/1738-single-field-fixed-size
Aug 27, 2026
Merged

mcintyre94 merged 2 commits into
anza-xyz:mainfrom
shin4141:fix/1738-single-field-fixed-size

Conversation

@shin4141

Copy link
Copy Markdown
Contributor

Problem

Single fixed-size field structs currently widen their literal fixedSize to number.

Context

A broader type-arithmetic solution was explored in #1761. Maintainer feedback identified exactly-one-fixed-size-field support as a simpler middle ground.

Summary of Changes

Add narrow overloads that forward the only field's existing literal fixedSize for getStructEncoder, getStructDecoder, and getStructCodec. Add focused typetests and a patch changeset for @solana/codecs-data-structures.

Multi-field structs intentionally retain their existing number behavior. This introduces no type arithmetic and no runtime behavior changes.

Tests

  • pnpm --filter @solana/codecs-data-structures test:typecheck
  • pnpm turbo compile:typedefs --filter=@solana/codecs-data-structures...
  • pnpm --filter @solana/codecs-data-structures test:lint
  • pnpm --filter @solana/codecs-data-structures test:prettier
  • pnpm --filter @solana/codecs-data-structures test:unit:node
  • pnpm --filter @solana/codecs-data-structures test:unit:browser
  • pnpm --filter @solana/codecs-data-structures compile:js, with Node, browser, and native runtime bundles byte-identical to the pristine build
  • Generated declaration consumer typecheck
  • pnpm changeset status
  • git diff --check

AI assistance

AI assistance was used during implementation and verification. Shin reviewed the final change and owns the submission.

Fixes #1738

Forward the only field’s literal fixedSize through struct encoder, decoder, and codec overloads while keeping multi-field structs on the existing number fallback. Add focused typetests and a patch changeset.
@changeset-bot

changeset-bot Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 18d0aeb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 48 packages
Name Type
@solana/codecs-data-structures Patch
@solana/codecs Patch
@solana/offchain-messages Patch
@solana/options Patch
@solana/program-client-core Patch
@solana/sysvars Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/instruction-plans Patch
@solana/kit Patch
@solana/signers Patch
@solana/programs Patch
@solana/react Patch
@solana/rpc-api Patch
@solana/rpc-subscriptions-api Patch
@solana/transaction-confirmation Patch
@solana/transaction-introspection Patch
@solana/wallet-account-signer Patch
@solana/compat Patch
@solana/plugin-interfaces Patch
@solana/rpc-graphql Patch
@solana/rpc Patch
@solana/rpc-subscriptions Patch
@solana/accounts Patch
@solana/addresses Patch
@solana/assertions Patch
@solana/codecs-core Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/errors Patch
@solana/fast-stable-stringify Patch
@solana/fixed-points Patch
@solana/functional Patch
@solana/instructions Patch
@solana/keys Patch
@solana/nominal-types Patch
@solana/plugin-core Patch
@solana/promises Patch
@solana/rpc-parsed-types Patch
@solana/rpc-spec-types Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/rpc-subscriptions-spec Patch
@solana/rpc-transformers Patch
@solana/rpc-transport-http Patch
@solana/rpc-types Patch
@solana/subscribable Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

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

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this! We need to tighten these typetests up a bit but this looks like a great solution to me!


import { getStructCodec, getStructDecoder, getStructEncoder } from '../struct';

type IsWidenedNumber<T extends number> = number extends T ? true : false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I appreciate what you're doing here, we want to check that the type is exactly Number and not a specific fixed size literal.

We have precedent for this with Equal:

type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;

Please could you copy over that and its long explanatory comment? It covers some edge cases that this one doesn't, and has a very detailed explanation

}

{
// [getStructEncoder/Decoder/Codec]: It only preserves literal sizes for exactly one fixed-size field.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please only test one of these at a time, so three different blocks for these tests

Comment on lines +95 to +97
true satisfies IsWidenedNumber<typeof encoder.fixedSize>;
true satisfies IsWidenedNumber<typeof decoder.fixedSize>;
true satisfies IsWidenedNumber<typeof codec.fixedSize>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be 3 blocks too, but to demonstrate how we'd use Equal here, this is how I'd write the encoder test:

{
    // [getStructEncoder]: Two fixed-size fields keep a widened size; it is not narrowed to a literal.
    const encoder = getStructEncoder([
        ['a', {} as FixedSizeEncoder<number, 4>],
        ['b', {} as FixedSizeEncoder<number, 8>],
    ]);
    encoder satisfies FixedSizeEncoder<{ a: number; b: number }>;
    true satisfies Equal<typeof encoder.fixedSize, number>;
}}

Use the repository’s strict Equal helper and separate encoder, decoder, and codec assertions so widened fixed sizes are checked exactly as number.
@shin4141

Copy link
Copy Markdown
Contributor Author

Thanks! Addressed in 18d0aeb. I copied the existing Equal helper and explanatory comment, split the encoder, decoder, and codec cases into separate blocks, and now assert exact number equality for the two-field, non-tuple, and empty widened-size cases. The follow-up commit only changes struct-typetest.ts; production and the changeset are untouched.

Re-ran package typecheck, compile:typedefs (6/6), lint (65/65), Prettier (70/70), Node and browser tests (303/303 each), the five runtime bundle hash checks (unchanged), and git diff --check.

@mcintyre94

Copy link
Copy Markdown
Member

@trevor-cortex

@bundlemon

bundlemon Bot commented Aug 27, 2026

Copy link
Copy Markdown

BundleMon

Unchanged files (150)
Status Path Size Limits
✅ @solana/kit production bundle
kit/dist/index.production.min.js
56.18KB -
✅ errors/dist/index.node.mjs
21.85KB -
✅ errors/dist/index.browser.mjs
21.82KB -
✅ errors/dist/index.native.mjs
21.82KB -
✅ rpc-graphql/dist/index.browser.mjs
18.87KB -
✅ rpc-graphql/dist/index.native.mjs
18.87KB -
✅ rpc-graphql/dist/index.node.mjs
18.86KB -
✅ wallet-account-signer/dist/index.node.mjs
18.53KB -
✅ wallet-account-signer/dist/index.native.mjs
18.51KB -
✅ wallet-account-signer/dist/index.browser.mjs
18.51KB -
✅ transaction-messages/dist/index.browser.mjs
11.34KB -
✅ transaction-messages/dist/index.native.mjs
11.34KB -
✅ transaction-messages/dist/index.node.mjs
11.34KB -
✅ instruction-plans/dist/index.browser.mjs
7.33KB -
✅ instruction-plans/dist/index.native.mjs
7.33KB -
✅ instruction-plans/dist/index.node.mjs
7.33KB -
✅ react/dist/index.browser.mjs
5.32KB -
✅ react/dist/index.native.mjs
5.32KB -
✅ react/dist/index.node.mjs
5.32KB -
✅ codecs-data-structures/dist/index.browser.mjs
5.29KB -
✅ codecs-data-structures/dist/index.native.mjs
5.29KB -
✅ codecs-data-structures/dist/index.node.mjs
5.29KB -
✅ offchain-messages/dist/index.browser.mjs
5.25KB -
✅ offchain-messages/dist/index.native.mjs
5.24KB -
✅ offchain-messages/dist/index.node.mjs
5.24KB -
✅ fixed-points/dist/index.browser.mjs
5.08KB -
✅ fixed-points/dist/index.native.mjs
5.07KB -
✅ fixed-points/dist/index.node.mjs
5.07KB -
✅ kit/dist/index.browser.mjs
4.69KB -
✅ kit/dist/index.native.mjs
4.69KB -
✅ kit/dist/index.node.mjs
4.69KB -
✅ transactions/dist/index.browser.mjs
4.05KB -
✅ transactions/dist/index.native.mjs
4.05KB -
✅ transactions/dist/index.node.mjs
4.05KB -
✅ codecs-core/dist/index.browser.mjs
3.63KB -
✅ codecs-core/dist/index.native.mjs
3.63KB -
✅ codecs-core/dist/index.node.mjs
3.62KB -
✅ webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
✅ webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
✅ webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
✅ rpc-subscriptions/dist/index.browser.mjs
3.37KB -
✅ rpc-subscriptions/dist/index.node.mjs
3.34KB -
✅ rpc-subscriptions/dist/index.native.mjs
3.31KB -
✅ signers/dist/index.browser.mjs
3.26KB -
✅ signers/dist/index.native.mjs
3.26KB -
✅ signers/dist/index.node.mjs
3.26KB -
✅ subscribable/dist/index.node.mjs
3.13KB -
✅ rpc-transformers/dist/index.browser.mjs
3.1KB -
✅ rpc-transformers/dist/index.native.mjs
3.1KB -
✅ rpc-transformers/dist/index.node.mjs
3.1KB -
✅ keys/dist/index.node.mjs
3.06KB -
✅ subscribable/dist/index.native.mjs
3.06KB -
✅ subscribable/dist/index.browser.mjs
3.05KB -
✅ addresses/dist/index.browser.mjs
2.93KB -
✅ addresses/dist/index.native.mjs
2.92KB -
✅ addresses/dist/index.node.mjs
2.92KB -
✅ keys/dist/index.browser.mjs
2.85KB -
✅ keys/dist/index.native.mjs
2.85KB -
✅ transaction-introspection/dist/index.browser.
mjs
2.73KB -
✅ transaction-introspection/dist/index.native.m
js
2.73KB -
✅ transaction-introspection/dist/index.node.mjs
2.73KB -
✅ codecs-strings/dist/index.browser.mjs
2.64KB -
✅ codecs-strings/dist/index.node.mjs
2.5KB -
✅ codecs-strings/dist/index.native.mjs
2.47KB -
✅ transaction-confirmation/dist/index.node.mjs
2.42KB -
✅ transaction-confirmation/dist/index.native.mj
s
2.37KB -
✅ sysvars/dist/index.browser.mjs
2.37KB -
✅ sysvars/dist/index.native.mjs
2.37KB -
✅ sysvars/dist/index.node.mjs
2.37KB -
✅ transaction-confirmation/dist/index.browser.m
js
2.36KB -
✅ rpc-subscriptions-spec/dist/index.node.mjs
2.33KB -
✅ rpc-subscriptions-spec/dist/index.native.mjs
2.29KB -
✅ rpc-subscriptions-spec/dist/index.browser.mjs
2.29KB -
✅ codecs-numbers/dist/index.browser.mjs
1.95KB -
✅ codecs-numbers/dist/index.native.mjs
1.95KB -
✅ codecs-numbers/dist/index.node.mjs
1.94KB -
✅ rpc/dist/index.node.mjs
1.94KB -
✅ rpc-types/dist/index.browser.mjs
1.9KB -
✅ rpc-types/dist/index.native.mjs
1.9KB -
✅ rpc-types/dist/index.node.mjs
1.9KB -
✅ rpc-transport-http/dist/index.browser.mjs
1.89KB -
✅ rpc-transport-http/dist/index.native.mjs
1.89KB -
✅ rpc/dist/index.native.mjs
1.8KB -
✅ rpc/dist/index.browser.mjs
1.79KB -
✅ rpc-transport-http/dist/index.node.mjs
1.71KB -
✅ rpc-spec-types/dist/index.browser.mjs
1.54KB -
✅ rpc-spec-types/dist/index.native.mjs
1.54KB -
✅ rpc-spec-types/dist/index.node.mjs
1.54KB -
✅ rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
✅ rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
✅ rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
✅ program-client-core/dist/index.browser.mjs
1.21KB -
✅ program-client-core/dist/index.native.mjs
1.21KB -
✅ program-client-core/dist/index.node.mjs
1.21KB -
✅ plugin-core/dist/index.browser.mjs
1.18KB -
✅ plugin-core/dist/index.native.mjs
1.18KB -
✅ plugin-core/dist/index.node.mjs
1.18KB -
✅ options/dist/index.browser.mjs
1.18KB -
✅ options/dist/index.native.mjs
1.18KB -
✅ options/dist/index.node.mjs
1.17KB -
✅ accounts/dist/index.browser.mjs
1.17KB -
✅ accounts/dist/index.native.mjs
1.17KB -
✅ accounts/dist/index.node.mjs
1.16KB -
✅ rpc-api/dist/index.browser.mjs
1.04KB -
✅ rpc-api/dist/index.native.mjs
1.04KB -
✅ rpc-api/dist/index.node.mjs
1.04KB -
✅ compat/dist/index.browser.mjs
969B -
✅ compat/dist/index.native.mjs
968B -
✅ compat/dist/index.node.mjs
966B -
✅ rpc-spec/dist/index.browser.mjs
928B -
✅ rpc-spec/dist/index.native.mjs
928B -
✅ rpc-spec/dist/index.node.mjs
926B -
✅ promises/dist/index.native.mjs
841B -
✅ promises/dist/index.node.mjs
840B -
✅ promises/dist/index.browser.mjs
839B -
✅ rpc-subscriptions-api/dist/index.browser.mjs
810B -
✅ rpc-subscriptions-api/dist/index.native.mjs
808B -
✅ rpc-subscriptions-api/dist/index.node.mjs
807B -
✅ assertions/dist/index.browser.mjs
783B -
✅ instructions/dist/index.browser.mjs
771B -
✅ instructions/dist/index.native.mjs
770B -
✅ instructions/dist/index.node.mjs
768B -
✅ fast-stable-stringify/dist/index.browser.mjs
726B -
✅ fast-stable-stringify/dist/index.native.mjs
725B -
✅ assertions/dist/index.native.mjs
724B -
✅ fast-stable-stringify/dist/index.node.mjs
724B -
✅ assertions/dist/index.node.mjs
723B -
✅ programs/dist/index.browser.mjs
329B -
✅ programs/dist/index.native.mjs
327B -
✅ programs/dist/index.node.mjs
325B -
✅ fs-impl/dist/index.browser.mjs
245B -
✅ event-target-impl/dist/index.node.mjs
230B -
✅ functional/dist/index.browser.mjs
154B -
✅ functional/dist/index.native.mjs
152B -
✅ text-encoding-impl/dist/index.native.mjs
152B -
✅ functional/dist/index.node.mjs
151B -
✅ codecs/dist/index.browser.mjs
145B -
✅ codecs/dist/index.native.mjs
144B -
✅ codecs/dist/index.node.mjs
142B -
✅ event-target-impl/dist/index.browser.mjs
133B -
✅ ws-impl/dist/index.node.mjs
131B -
✅ text-encoding-impl/dist/index.browser.mjs
122B -
✅ fs-impl/dist/index.node.mjs
120B -
✅ text-encoding-impl/dist/index.node.mjs
119B -
✅ ws-impl/dist/index.browser.mjs
113B -
✅ crypto-impl/dist/index.node.mjs
111B -
✅ crypto-impl/dist/index.browser.mjs
109B -
✅ rpc-parsed-types/dist/index.browser.mjs
66B -
✅ rpc-parsed-types/dist/index.native.mjs
65B -
✅ rpc-parsed-types/dist/index.node.mjs
63B -

No change in files bundle size

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@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.

This PR adds a narrow overload to getStructEncoder, getStructDecoder, and getStructCodec that forwards the field's literal fixedSize when the struct has exactly one fixed-size field, plus focused typetests and a patch changeset. No runtime changes — overloads only.

The solution looks solid and the typetests are now nicely tightened per the prior review — this would be an approval, but I don't have approve permissions on this repo:

  • Overload resolution is correct. The single-element-tuple constraint (readonly [readonly [string, FixedSizeEncoder<any>]]) only matches literal one-field tuples. Multi-field tuples, non-tuple Fields<...> arrays, and [] all fall through to the existing widened overloads — and the new Equal-based typetests pin each of those cases down exactly, which satisfies number alone couldn't do (a literal 4 would still satisfy number).
  • No generic-call-site regressions. A call with a generic TFields extends Fields<FixedSizeEncoder<any>> doesn't match the tuple overload and resolves to the widened one, same as before.
  • Changeset level looks right. Narrowing number to a literal is non-breaking for consumers (FixedSizeEncoder<T, 4> remains assignable to FixedSizeEncoder<T, number>), so patch fits.

Two minor nits inline, neither blocking: the Equal helper's doc comment describes rationale that doesn't match its actual use in this file, and the JSDoc attachment in struct.ts shifts to the new overload (worth a quick hover check in an editor).

For subsequent reviewers: the main thing to verify is that the generated .d.ts output keeps the overload order intact — the narrow overload must stay first for resolution to work in consuming packages. The PR author reports a declaration-consumer typecheck was run, which covers this.

@mcintyre94 mcintyre94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome, thanks for this! 🚀

@mcintyre94
mcintyre94 enabled auto-merge August 27, 2026 16:25
@mcintyre94
mcintyre94 added this pull request to the merge queue Aug 27, 2026
Merged via the queue into anza-xyz:main with commit c51b9f6 Aug 27, 2026
11 checks passed
This was referenced Aug 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Sep 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getStructEncoder/Decoder/Codec do not infer a literal fixedSize from their fields

3 participants