Legacy union constructors build a broken object instead of throwing, on all 115 generated union types
Severity: Low. Code-only — the documentation is already correct and complete. Tested on 17.0.0-rc.1 from npm; Node 22.22.0, Deno 2.8.3, Bun 1.3.14 identical.
v17 replaced new xdr.Union(discriminant, value) with arm factories (xdr.TransactionMeta.v3(value)). The old form does not throw — it returns an instance of the abstract base, silently discarding both arguments:
const m = new xdr.TransactionMeta(3, someValue);
m.constructor.name; // "TransactionMetaBase"
Object.keys(m); // [] <-- discriminant and value are gone
m.type; // undefined
m.toXdr(); // TypeError: this.toXdrObject is not a function
toJson() and toXdr("base64") throw the identical TypeError, all three from lib/esm/xdr/values/xdr-value.js:11 — inside the SDK's own serialization, naming neither the union nor the call that created it. Because the broken object is constructed successfully and only fails when serialized, the throw can surface arbitrarily far from the responsible new xdr.TransactionMeta(...) — including as a whole module failing at import, when the call sits in a module-level constant.
Every generated union is affected. A union is exported under a name whose class is the abstract <Name>Base; sweeping those and calling the legacy form gives 115 of 115:
import { xdr } from "@stellar/stellar-sdk";
const unions = Object.entries(xdr).filter(([k, v]) => typeof v === "function" && v.name === k + "Base");
let broken = 0;
for (const [k, U] of unions) {
const o = new U(0, null);
if (o.constructor.name === k + "Base" && Object.keys(o).length === 0 && o.type === undefined) broken++;
}
console.log(broken, "of", unions.length); // 115 of 115 — none throw, none retain the arguments
grep -c "declare abstract class .*Base" lib/esm/xdr/generated/*.d.ts totals the same 115. ScVal, Asset, Memo, AccountEntryExt, OperationBody, PublicKey and LedgerKey are all included. (The 580 concrete arm classes — AccountEntryExtV0 and the like — are a separate surface and are not what this issue is about.)
The SDK already guards the equivalent mistake. xdr.Int64 / Uint64 / Int32 throw at the call site naming the fix, and enums fail immediately too — xdr.AssetType.assetTypeNative is the enum singleton rather than a factory, so the legacy call raises TypeError: xdr.AssetType.assetTypeNative is not a function:
TypeError: new xdr.Int64(...) is not supported: XDR int64 values are native bigints.
Call xdr.Int64(value) (no `new`) or pass a bigint literal instead.
§ 4 states the rationale — "a boxed bigint would fail deep inside serialization instead of at the call site" — which applies verbatim to unions.
Exposure: plain JavaScript, and TypeScript run without a type-check pass. The declarations already say these bases are abstract (declare abstract class TransactionMetaBase extends XdrValue in transaction-meta.d.ts:42), so new xdr.TransactionMeta(3, v) is TS2511: Cannot create an instance of an abstract class — the runtime simply does not enforce what the .d.ts promises. The documented narrowing helpers also behave correctly (isUnionVariant → false, expectUnionVariant → Expected XDR variant 'v3', got 'undefined' (TransactionMetaBase)). The difficulty is localization, not comprehension — § 2 and § 10 already give the right replacement. TransactionMeta.is(m) returns true, since it tests instanceof TransactionMetaBase and the broken object genuinely is one.
Reproduce
import { xdr } from "@stellar/stellar-sdk";
const m = new xdr.TransactionMeta(3, null);
console.log(m.constructor.name, Object.keys(m), m.type); // TransactionMetaBase [] undefined
m.toXdr(); // TypeError: this.toXdrObject is not a function
Suggested fix
Give the generated union base classes a constructor that throws in the same shape as xdr.Int64, naming the arm factories — e.g. new xdr.TransactionMeta(...) is not supported: call xdr.TransactionMeta.v3(value) instead. Applying an established pattern to the remaining 115; every legacy call site then fails at the line that needs changing.
Legacy union constructors build a broken object instead of throwing, on all 115 generated union types
Severity: Low. Code-only — the documentation is already correct and complete. Tested on
17.0.0-rc.1from npm; Node 22.22.0, Deno 2.8.3, Bun 1.3.14 identical.v17 replaced
new xdr.Union(discriminant, value)with arm factories (xdr.TransactionMeta.v3(value)). The old form does not throw — it returns an instance of the abstract base, silently discarding both arguments:toJson()andtoXdr("base64")throw the identicalTypeError, all three fromlib/esm/xdr/values/xdr-value.js:11— inside the SDK's own serialization, naming neither the union nor the call that created it. Because the broken object is constructed successfully and only fails when serialized, the throw can surface arbitrarily far from the responsiblenew xdr.TransactionMeta(...)— including as a whole module failing at import, when the call sits in a module-level constant.Every generated union is affected. A union is exported under a name whose class is the abstract
<Name>Base; sweeping those and calling the legacy form gives 115 of 115:grep -c "declare abstract class .*Base" lib/esm/xdr/generated/*.d.tstotals the same 115.ScVal,Asset,Memo,AccountEntryExt,OperationBody,PublicKeyandLedgerKeyare all included. (The 580 concrete arm classes —AccountEntryExtV0and the like — are a separate surface and are not what this issue is about.)The SDK already guards the equivalent mistake.
xdr.Int64/Uint64/Int32throw at the call site naming the fix, and enums fail immediately too —xdr.AssetType.assetTypeNativeis the enum singleton rather than a factory, so the legacy call raisesTypeError: xdr.AssetType.assetTypeNative is not a function:§ 4 states the rationale — "a boxed bigint would fail deep inside serialization instead of at the call site" — which applies verbatim to unions.
Exposure: plain JavaScript, and TypeScript run without a type-check pass. The declarations already say these bases are abstract (
declare abstract class TransactionMetaBase extends XdrValueintransaction-meta.d.ts:42), sonew xdr.TransactionMeta(3, v)isTS2511: Cannot create an instance of an abstract class— the runtime simply does not enforce what the.d.tspromises. The documented narrowing helpers also behave correctly (isUnionVariant→false,expectUnionVariant→Expected XDR variant 'v3', got 'undefined' (TransactionMetaBase)). The difficulty is localization, not comprehension — § 2 and § 10 already give the right replacement.TransactionMeta.is(m)returnstrue, since it testsinstanceof TransactionMetaBaseand the broken object genuinely is one.Reproduce
Suggested fix
Give the generated union base classes a constructor that throws in the same shape as
xdr.Int64, naming the arm factories — e.g.new xdr.TransactionMeta(...) is not supported: call xdr.TransactionMeta.v3(value) instead. Applying an established pattern to the remaining 115; every legacy call site then fails at the line that needs changing.