Skip to content

Allow narrower boolean predicates in pattern match codecs - #1809

Merged
mcintyre94 merged 2 commits into
mainfrom
narrower-pattern-match-predicates
Jul 13, 2026
Merged

Allow narrower boolean predicates in pattern match codecs#1809
mcintyre94 merged 2 commits into
mainfrom
narrower-pattern-match-predicates

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Boolean predicates passed to getPatternMatchCodec and getPatternMatchEncoder previously had to be typed against the full value type of the matched variants. Because the number codecs accept number | bigint, matching against them forced predicates such as (value: number | bigint) => ... even when the caller only handles number, unlike getPredicateCodec/getPredicateEncoder which infer the predicate type independently and accept (value: number) => ....

This aligns the two APIs by checking the boolean predicate parameter bivariantly via a method-syntax escape hatch, so a predicate that narrows to a subtype of the variant's value type is accepted. Codec and encoder assignability are untouched, so the existing cross-branch type-consistency check and size inference still hold. Adds typetests covering the new behavior for both the encoder and codec, and reverts the docs example back to plain (value: number).

After this we can update the docs back: #1810

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1acc0d9

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/rpc-graphql Patch
@solana/plugin-interfaces 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 commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

@mcintyre94
mcintyre94 marked this pull request as ready for review July 1, 2026 13:59
@bundlemon

bundlemon Bot commented Jul 1, 2026

Copy link
Copy Markdown

BundleMon

Unchanged files (150)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
54.87KB -
errors/dist/index.node.mjs
21.55KB -
errors/dist/index.browser.mjs
21.53KB -
errors/dist/index.native.mjs
21.52KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.82KB -
rpc-graphql/dist/index.node.mjs
18.82KB -
wallet-account-signer/dist/index.node.mjs
18.28KB -
wallet-account-signer/dist/index.browser.mjs
18.26KB -
wallet-account-signer/dist/index.native.mjs
18.26KB -
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.02KB -
instruction-plans/dist/index.native.mjs
7.02KB -
instruction-plans/dist/index.node.mjs
7.02KB -
codecs-data-structures/dist/index.browser.mjs
5.3KB -
codecs-data-structures/dist/index.native.mjs
5.3KB -
codecs-data-structures/dist/index.node.mjs
5.29KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
offchain-messages/dist/index.browser.mjs
5.06KB -
offchain-messages/dist/index.native.mjs
5.06KB -
offchain-messages/dist/index.node.mjs
5.06KB -
react/dist/index.browser.mjs
5.02KB -
react/dist/index.node.mjs
5.02KB -
react/dist/index.native.mjs
5.02KB -
kit/dist/index.browser.mjs
4.6KB -
kit/dist/index.native.mjs
4.6KB -
kit/dist/index.node.mjs
4.6KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
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 -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.15KB -
keys/dist/index.node.mjs
3.06KB -
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 -
subscribable/dist/index.node.mjs
2.8KB -
transaction-introspection/dist/index.browser.
mjs
2.75KB -
transaction-introspection/dist/index.native.m
js
2.75KB -
transaction-introspection/dist/index.node.mjs
2.75KB -
subscribable/dist/index.native.mjs
2.73KB -
subscribable/dist/index.browser.mjs
2.73KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
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 -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.23KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.19KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.19KB -
rpc/dist/index.node.mjs
1.95KB -
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-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.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
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 -
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-spec-types/dist/index.browser.mjs
1.15KB -
rpc-spec-types/dist/index.native.mjs
1.15KB -
rpc-spec-types/dist/index.node.mjs
1.15KB -
rpc-api/dist/index.browser.mjs
1015B -
rpc-api/dist/index.native.mjs
1014B -
rpc-api/dist/index.node.mjs
1012B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec/dist/index.browser.mjs
898B -
rpc-spec/dist/index.native.mjs
897B -
rpc-spec/dist/index.node.mjs
896B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
plugin-core/dist/index.browser.mjs
799B -
plugin-core/dist/index.native.mjs
798B -
plugin-core/dist/index.node.mjs
796B -
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

@mcintyre94

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

Aligns getPatternMatchCodec / getPatternMatchEncoder predicate typing with getPredicateCodec / getPredicateEncoder by making the boolean-predicate parameter bivariant. Previously, predicates had to be typed against the full value type of the matched variants (e.g. (value: number | bigint) => ... for the number codecs), whereas the sibling predicate APIs already infer the predicate type independently and accept (value: number) => .... The fix introduces a BivariantBooleanPredicate<TFrom> helper ({ predicate(value: TFrom): boolean }['predicate'] — a method-syntax escape hatch that reintroduces bivariance under strictFunctionTypes) and uses it for the boolean-predicate branches of both PatternMatchEncoderEntry and PatternMatchCodecEntry. Encoder/codec assignability is untouched, so the existing cross-branch type-consistency check and size inference still hold.

Correctness

  • The bivariant trick is narrowly scoped to the predicate parameter only — the encoder/codec slot in the tuple is unchanged, so the invariant that all boolean-predicate branches must share the same TFrom (and TTo for codecs) is preserved. The existing @ts-expect-error case for mixing Encoder<number> and Encoder<string> still fires, confirmed by the untouched negative typetests.
  • The order of the two union members was swapped so that the type-guard variant ((value: TFrom) => value is TNarrowed) comes first and the boolean variant second. This is semantically neutral for assignability (both are alternatives in a union) but a nicer default for error messages. A type-guard is trivially assignable to the boolean-predicate slot too now that the parameter is bivariant, but since the codec/encoder type in each tuple variant still pins the correct shape, there's no accidental widening.
  • The runtime code is entirely unchanged; this is a pure type-level refinement.

Consistency with getPredicateCodec

Worth noting for future readers: this brings the same runtime foot-gun that getPredicateCodec already has — a caller can now type a predicate more narrowly than the codec's actual value type, and at runtime the predicate will be invoked with values outside its declared parameter type (e.g. a (v: number) => v < 256 predicate can be handed a bigint and will throw a TypeError on the comparison). That's an accepted tradeoff for ergonomics and is consistent with the sibling API, but it's the reason to keep the boolean-predicate path opt-in via a plain arrow (versus type-guard predicates which retain full parameter variance).

Test coverage

The new typetest blocks — one for getPatternMatchEncoder, one for getPatternMatchCodec — exercise exactly the scenario from the PR description: a (value: number) => boolean predicate paired with an Encoder<bigint | number> / Codec<bigint | number, number>. The existing negative typetests for cross-branch type mismatches still pass, which is the key safety property.

The docs example revert to (value: number) is a nice touch — the previous (value: number | bigint) was a workaround for exactly this issue.

Changeset is present and correctly scoped to @solana/codecs-data-structures as a patch (this is a strict typing relaxation with no runtime or API changes).

Notes for subsequent reviewers

  • Verify the two new typetest blocks compile as expected (they should — the bivariant method syntax is a well-known TS escape hatch used elsewhere in the ecosystem, e.g. React's event handler types).
  • Nothing to check on the docs change beyond confirming the code snippet still renders correctly on solanakit.com after publish.

LGTM — clean, well-scoped, and the test coverage matches the change surface.

Boolean predicates passed to `getPatternMatchCodec` and `getPatternMatchEncoder` previously had to be typed against the full value type of the matched variants. Because the number codecs accept `number | bigint`, matching against them forced predicates such as `(value: number | bigint) => ...` even when the caller only handles `number`, unlike `getPredicateCodec`/`getPredicateEncoder` which infer the predicate type independently and accept `(value: number) => ...`.

This aligns the two APIs by checking the boolean predicate parameter bivariantly via a method-syntax escape hatch, so a predicate that narrows to a subtype of the variant's value type is accepted. Codec and encoder assignability are untouched, so the existing cross-branch type-consistency check and size inference still hold. Adds typetests covering the new behavior for both the encoder and codec, and reverts the docs example back to plain `(value: number)`.
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview: https://kit-docs-cngdfs1m3-anza-tech.vercel.app

Base automatically changed from new-docs-twoslash to main July 1, 2026 15:01
@mcintyre94
mcintyre94 requested a review from lorisleiva July 6, 2026 10:12

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

Thank you!

Comment on lines +12 to +15
// A boolean predicate whose parameter is checked bivariantly (method-syntax escape hatch), so a
// predicate that narrows to a subtype of the variant's value type — e.g. `(value: number)` against a
// number codec whose value type is `number | bigint` — is still accepted, mirroring `getPredicateCodec`.
type BivariantBooleanPredicate<TFrom> = { predicate(value: TFrom): boolean }['predicate'];

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.

Interesting TS trick. TIL. ☺️

@mcintyre94
mcintyre94 added this pull request to the merge queue Jul 13, 2026
Merged via the queue into main with commit 204ed6e Jul 13, 2026
14 checks passed
@mcintyre94
mcintyre94 deleted the narrower-pattern-match-predicates branch July 13, 2026 12:23
@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 Jul 28, 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.

3 participants