Add UnwrapRpcResponse<T> + splitSolanaRpcResponse() to @solana/rpc-types#1678
Add UnwrapRpcResponse<T> + splitSolanaRpcResponse() to @solana/rpc-types#1678mcintyre94 wants to merge 1 commit into
UnwrapRpcResponse<T> + splitSolanaRpcResponse() to @solana/rpc-types#1678Conversation
🦋 Changeset detectedLatest commit: b80e471 The changes in this PR will be included in the next version bump. This PR includes changesets to release 47 packages
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 |
BundleMonFiles updated (25)
Unchanged files (122)
Total files change +6.05KB +1.15% Final result: ✅ View report in BundleMon website ➡️ |
4e03c8b to
c11b360
Compare
|
Documentation Preview: https://kit-docs-43w5jsdrp-anza-tech.vercel.app |
…pc-types`
Adds a co-located type-level and runtime decomposition for the `SolanaRpcResponse<T>` envelope, so callers can handle notifications that may or may not be wrapped without re-implementing the duck-type check.
`UnwrapRpcResponse<T>` is a conditional type that unwraps `SolanaRpcResponse<U>` → `U` and passes through other shapes unchanged. `splitSolanaRpcResponse()` runtime-detects the envelope via duck-type (`'context' in x && 'value' in x`) and decomposes into the inner value and the lifted slot. Raw notifications pass through with `slot: undefined`; `undefined` input returns both halves as `undefined`. Four overloads narrow the return type to match the input: `SolanaRpcResponse<T>` → `{ slot: Slot; value: T }`; `undefined` → `{ slot: undefined; value: undefined }`; `SolanaRpcResponse<T> | undefined` → `{ slot: Slot | undefined; value: T | undefined }`; raw `T` → `{ slot: undefined; value: T }`. The third overload lets callers pipe a possibly-undefined source straight through without an external null-check.
T is intentionally unconstrained — the runtime check handles arbitrary shapes, and the overloads surface precise return types per call site. Tests cover the envelope path, raw passthrough, `undefined`/`null` inputs, malformed shapes (missing `context` or `value`), primitives, and envelopes whose `value` itself is `undefined` / `null`.
c11b360 to
b80e471
Compare
efde42d to
2fd75aa
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
Adds two helpers to @solana/rpc-types:
UnwrapRpcResponse<T>— conditional type that unwrapsSolanaRpcResponse<U>toU, passes other types through.splitSolanaRpcResponse()— runtime helper that duck-types the envelope shape ('context' in x && 'value' in x) and decomposes into{ value, slot }. Raw inputs pass through withslot: undefined;undefinedreturns both halves asundefined.
Four overloads cover the envelope, undefined, SolanaRpcResponse<T> | undefined, and raw T cases. Comes with a thorough unit test file and a small type test, plus a minor changeset.
Things to watch out for / notes for subsequent reviewers
1. Missing README update. The package's README.md documents every existing public type and function but isn't updated here. Per the repo's TypeScript READMEs skill ("When adding a new public API, add or update the package's README"), this PR should add UnwrapRpcResponse under "Types" and splitSolanaRpcResponse under "Functions". The JSDoc is solid, but the README is the user-facing entry point.
2. Type-level gap for genuinely mixed unions. The PR description says these helpers handle "an input that may or may not be a SolanaRpcResponse". The overloads cover SolanaRpcResponse<T>, undefined, SolanaRpcResponse<T> | undefined, and raw T — but not SolanaRpcResponse<T> | T (i.e. the case where the call site genuinely doesn't know which it has). With a union like SolanaRpcResponse<X> | RawX, none of overloads 1–3 match (each arm fails them), so it falls through to overload 4 (notification: T) and you get { slot: undefined; value: SolanaRpcResponse<X> | RawX } — slot is statically undefined even though at runtime the envelope arm would produce a real slot.
For the React-hook use case (a single subscription whose envelope-ness is statically known) this is fine. But if the intent is broader, consider adding an overload before overload 4 along the lines of:
export function splitSolanaRpcResponse<TRaw>(
notification: SolanaRpcResponse<TRaw> | TRaw
): { slot: Slot | undefined; value: TRaw };A matching type-test case would also be worth adding so the resolution doesn't silently regress.
3. Overload ordering is correct, but fragile. Overload 4 (<T>(notification: T)) is broad enough to swallow envelope inputs if TS ever picked it. It works today because earlier overloads come first and TS picks the first matching signature, but it's worth a comment explaining the ordering so a future contributor doesn't reorder them. Adding the union overload above would push overload 4 even further down — also fine, just keep it as the final fallback.
4. Minor: null handling is implicit. The runtime guard (notification != null) accepts null, but the public overloads don't list it; the test for null casts via as unknown as undefined. Either extend the undefined overload to null | undefined and add a real null overload, or have the test cover only what the type contract promises. The current state is slightly misleading.
5. Naming. splitSolanaRpcResponse is descriptive, but the function also unwraps — unwrapSolanaRpcResponse would arguably read better as a callsite verb (const { value, slot } = unwrapSolanaRpcResponse(...)). Subjective, not blocking.
Nothing here is a correctness issue — the runtime behaviour and tests are tight. Main asks are the README update and a decision on whether the mixed-union case is in scope.

Summary of Changes
This PR adds a new type
UnwrapRpcResponsewhich unwrapsSolanaRpcResponse<T>toTand leaves any other type unchanged. In practice this allows extracting the inner value type from an input that may or may not be aSolanaRpcResponse.Alongside this it adds a new function
splitSolanaRpcResponsewhich takes an input that may be aSolanaRpcResponseand unwraps it to{value, slot: Slot}. If the input is notSolanaRpcResponsethen it is returned asvaluewithslot: undefined. The input can be undefined to allow bridging from a store with optional data.These helpers are intended for processing RPC subscriptions in a react hook, but they apply to RPC requests with the
SolanaRpcResponsewrapper too.