Read the direction before trusting a tsc verdict. Assignability
to WireValue is sufficient for domain membership and is not
necessary: everything the type admits is in the domain, and the
converse does not hold. It cannot be made to hold in TypeScript. The
object arm has to be an index signature — that is what makes the type
recursive and what makes it refuse { cb: () => void } — and
TypeScript grants an implicit index signature to an anonymous object
type or a type alias, but never to an interface or a class:
type RowA = { id: number } // assignable
interface RowB { id: number } // TS2322 — identical members, refused
class RowC { id = 1 } // TS2322 — refused
All three are the same plain object at run time and the relation walks
all three identically: the shipped content-hash gives new RowC() and
{ id: 1 } one verdict (packages/core/wasm/content-hash.ts), which
is what §5.1 Amendment 8 records as a permitted quotient. So a TS2322
here is a statement about TypeScript's assignability rules, never a
statement about the domain — an interface-declared row type is
INSIDE it. Adopters who want the check anyway: declare the shape as a
type alias, or widen at the use site with a spread. This paragraph
exists because an omission is not a denial — a reader who found no
disclaimer here would read the refusal as a domain verdict, which is
exactly backwards. test/value-domain-open-270.test.ts pins both
halves so the type and this text cannot drift apart.
undefined is a member position, never a value. The container
arms read WireValue | undefined; the union head does not. That is
what admits { a: undefined }, Record<string, T | undefined>, a
Set/Map holding undefined, and the hole in [ , 1] — none of
which have any spelling this type could otherwise reach, and all of
which the shipped relation already places on a verdict it also gives a
null-spelled twin it admitted all along. A bare top-level
undefined is different: isWireNull collapses it to the NULL wire
record before anything structural runs, so it stays out of the head
and undefined on its own is still refused. bigint, symbol and
function stay out of member positions too — admitting them would
leave the recursion excluding nothing, which is the only work the type
does.
This is NOT a bound on Graph.input, Graph.derived or
Compute, and it must not become one. #270 settled that
question: the adopter-facing value domain is OPEN and WireValue
names the cutoff/marshal domain only. The decision, and the cost of
the alternative, are recorded once in SPEC §15.1's #270 amendment.
The four non-JSON scalar classes — undefined, bigint, symbol
and function — sit OUTSIDE WireValue as top-level values (see the
member-position remark above for the one place undefined is inside)
and are nevertheless admitted at the adopter surface.
packages/core/src/value-domain.ts
is where that split is implemented: isWireNull collapses all four to
the single NULL wire record so the two engines agree on whether a write
fires, while the value itself is retained host-side and read returns
the real reference on both engines. A closure-valued input reads back
under Object.is as the same function it was written with; the
coercion #151 shipped went into the cutoff, never into read.
Earlier plans named this constraint JsonRoundtrippable and proposed
it as a Node<T> bound. That name is JSON-only, so as a bound it would
have refused Set, Map, Date and Temporal — four types both
engines already carry correctly — on its first day. WireValue is the
corrected name for the domain that actually exists; there is no bound.
WireTemporal for the Temporal.* member's shape.
A TypeScript sufficient condition for membership of the wire value domain — the values that cross the FFI boundary intact, and the values §5.1 Amendment 7's derived-cutoff relation ranges over (
SPEC.md, "structural deep-equality over the shipped wire value domain").It is JSON —
null/boolean/number/string, arrays, plain objects — extended by the four tagged containers the bridge carries:Set,Map,DateandTemporal.*.packages/core/wasm/tagged-types.tsrewrites each of those four into a__causlType-tagged plain object on the way into the value pool and reconstructs the real instance on the way out, so all four are INSIDE the domain, compare element-wise, and round-trip as real instances on both shipped engines.