Serialization-boundary marker for rows, collections and nullable fields
Zero-runtime marker for the serialization-safe shape at DB / HTTP / JSONB boundaries. Grep-able, ESLint-aware, no casts at call sites.
A serialization-boundary marker for values crossing DB rows, HTTP bodies, JSONB payloads and workflow inputs. It wraps any boundary shape: a collection, a nullable field, or a whole row type. Zero runtime cost, assignable both ways with the plain type, no casts at call sites.
declare const WIRE: unique symbol;
export interface WireMark {
readonly [WIRE]?: never;
}
export type Wire<T> = T | (T & WireMark);
Wire is a flavored type, not a branded one. The plain T member means Wire<T> and T accept each other without ceremony, including null, undefined and unknown, so generic helpers like findOne<R>(): Promise<Wire<R | null>> compile. null is exactly the shape that survives JSON.stringify.
The T & WireMark member is there for naming, not safety. It keeps TypeScript from resolving the alias away, so inferred exports emit Wire<Row> in .d.ts files and hovers.
The value doesn’t come from type-level enforcement. It comes from three places:
rg 'Wire<' is a complete, countable inventory of every serialization boundary in the codebase.type UserRow = Wire<{ … }> states the boundary in exactly one place.functype/prefer-list and functype/prefer-option skip everything inside a Wire<…> type argument. A ReadonlyArray or a T | null there is the declared wire shape, not a missed List or Option.import type { Wire } from "functype";
// or
import type { Wire } from "functype/wire";
// A whole row or DTO: every field inside is covered.
type UserRow = Wire<{
readonly id: string;
readonly email: string | null;
readonly tags: ReadonlyArray<string>;
}>;
// A collection.
type UserRows = Wire<ReadonlyArray<UserRow>>;
// A single field on an otherwise domain-shaped type.
type Invite = {
readonly id: InviteId;
readonly acceptedAt: Wire<string | null>;
};
// eslint.config.mjs — hold the enforcement, mark the boundaries
{
"functype/prefer-list": ["error", { "allowReadonlyArrays": false }],
"functype/prefer-option": "warn", // "error" once your boundary types are wired
}
// Declare the boundary once.
async function fetchUsers(): Promise<UserRows> {
return db.query("SELECT * FROM users").rows;
}
// Inside the pipeline, convert to List and Option for transformation.
const reachable = List(await fetchUsers())
.filter((u) => Option(u.email).isSome())
.map(enrich);
Both rules take wireTypes (default ["Wire"]). Add your own boundary alias name if you have one, or pass [] to turn the exemption off. Qualified references (functype.Wire<…>) are recognized.
In 1.9, Wire<T> meant ReadonlyArray<T>. In 1.10 it wraps any shape, so:
type ClaimedDocs = Wire<Row>; // 1.9: an array of Row
type ClaimedDocs = Wire<ReadonlyArray<Row>>; // 1.10
The break is loud, not silent: a Wire<Row> that meant an array fails to compile at the first .map or array assignment. The two exceptions are Wire<object> and Wire<any>, which accept an array under either meaning; search for them when upgrading.
List<T> / Option<T> first.