TypeScript Runtime Trust Boundaries: Versioned Decoding, Resource Limits, and Dual-Read Rollbacks
Type declarations do not validate network, Worker, URL, IndexedDB, or cross-tab messages. This article converts unknown input into canonical internal types and designs schema evolution, dual-read rollouts, and rollback paths for legacy data.
Key Points
- Topic Focus: AI、TypeScript、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
fetch<User>(), event.data as Result, and JSON.parse(raw) as Draft do not inspect runtime bytes. Server-side canary rollouts, stale tabs, corrupted IndexedDB data, or URL parameters can all deliver values with the wrong shape. Advanced TypeScript Types constrain controlled code; this article addresses external trust boundaries.
Every entry point starts with unknown
Network responses, Workers, BroadcastChannel, URLs, postMessage, and local records all start as unknown. Entry-point adapters enforce resource limits, identify the version, invoke a decoder, and then convert the result into a stable internal type. Business components never touch raw objects.
type CanonicalTask = {
id: string;
prompt: string;
priority: "interactive" | "background";
};
const taskV2 = z.object({
schemaVersion: z.literal(2),
id: z.string().regex(/^tsk_[a-z0-9]{12}$/),
input: z.object({ prompt: z.string().min(1).max(20_000) }).strict(),
priority: z.enum(["interactive", "background"]),
}).strict();
function decodeTask(raw: unknown): CanonicalTask {
const parsed = taskV2.parse(raw);
return { id: parsed.id, prompt: parsed.input.prompt, priority: parsed.priority };
}
Zod works well for colocated declarations, while JSON Schema is better suited to cross-language protocols. Security comes from runtime validation, strict field policies, and semantic checks, not from type generation. Even when model output uses JSON mode, the complete record must still be validated after it is received; see Structured Outputs.
Enforce resource limits at the points each entry point allows
A schema validator is not a traffic shaper. For network byte streams, you can constrain the status code, Content-Type, actual decompressed bytes read, and timeout before allocating a JSON object. You cannot rely solely on Content-Length. After parsing a URL, you can also immediately limit its total length, number of repeated parameters, and allowed keys. These are hard prevalidation limits that the application can actually enforce.
Worker message events, BroadcastChannel, and IndexedDB reads are different: the browser has already performed structured cloning or deserialization before the handler or decoder receives the value. Receiver-side checks cannot undo that allocation. Serializing event.data again to "estimate its size" repeats traversal and allocation and may also encounter cyclic structures. Receiver-side limits on array elements, string length, nesting depth, and deadlines remain valuable, but they constrain only subsequent decoder work, business CPU usage, and logging. They should not be presented as the initial memory defense.
For these entry points, use a narrow envelope on the sending or writing side and prefer transmitting IDs, digests, and versions. Senders should use credits based on outstanding tasks and bytes, while IndexedDB writes should limit the verifiable encoded size. If the sender cannot be trusted, move subsequent parsing into a terminable Worker and limit the source, rate, and concurrency. Isolation can reduce sustained memory usage and main-thread blocking, but it cannot prevent a clone allocation that has already occurred.
After structural validation, also verify cross-field invariants such as chronological ordering, cursor ownership, model dimensions, and tenant identity. .strict() or additionalProperties: false can detect unexpected fields, but the compatibility policy must consistently decide whether to reject or safely discard them. The bounded parsing described in Sanitizing Agent Tool Outputs also applies to browser messages.
Errors should record only the boundary, schema version, code, field path, byte range, and payload hash, never the complete prompt, URL token, or IndexedDB record. Group rejection rates by application version and source.
Give every external version its own decoder
Do not create a "universal schema" filled with optional fields. For example, if v1 uses text and v2 uses input.prompt, retain two strict decoders and map both to CanonicalTask:
function decodeAnyTask(raw: unknown): CanonicalTask {
const version = readBoundedVersionDiscriminator(raw);
if (version === 1) return fromV1(taskV1.parse(raw));
if (version === 2) return fromV2(taskV2.parse(raw));
throw new DecodeError("UNSUPPORTED_SCHEMA_VERSION");
}
readBoundedVersionDiscriminator reads only an integer own property from a plain object and does not traverse the prototype. Conversion functions normalize defaults and enum values while enforcing internal invariants; the domain layer no longer carries a v1/v2 union type. This matches REST API Versioning, but browsers must also support cached bundles and offline records.
IndexedDB stores both schemaVersion and the application version that wrote the record. onupgradeneeded performs structural upgrades only; data migration uses resumable cursors and small transactions. BroadcastChannel messages include the protocol, sender, and sequence, and their generation is determined only after decoding.
Roll out by expanding first, writing next, and contracting last
In the first phase, readers support both v1 and v2, but writers produce only v1. In the second phase, a small percentage of traffic writes v2, while network, Worker, and IndexedDB paths continue to read both versions. Offline records are migrated lazily, and hashes are recorded for failures. Remove the v1 decoder only after the compatibility window, stale-tab lifecycle, and offline retention period have all ended.
For rollback, first switch the writer back to v1, then restore the previous logic. The rollback bundle must retain the v2 decoder because the new format may already have been written locally or sent by another tab. Fields that cannot be represented may only be degraded to approved values; permissions, monetary amounts, and execution states must never be silently lost.
Use boundary fixtures to prove there are no type illusions
Tests should cover missing and unknown versions, extra fields, invalid enum values, overlong strings, enormous arrays, deeply nested objects, prototype-pollution keys, cyclic clones, invalid URLs, truncated JSON, old IndexedDB records, and out-of-order messages. A cyclic object can legally reach the handler; a shallow gate or isolated Worker must reject it within the deadline without serializing the original payload again. A decoder may return only an object that satisfies all invariants or a bounded error. It must never partially update the UI.
End-to-end rehearsals should run old and new pages side by side, write v2 data, and then load the rollback bundle to confirm that the data remains readable and is not written back in a corrupted version. Next, have the Worker and network return different generations for the same id to verify concurrency isolation. Input becomes trusted only after version-specific validation and canonical conversion. For objects that have already been cloned, this constrains only subsequent use; it does not guarantee that memory was not allocated earlier.
Further reading
Related
Related Content
Article
What Is Vibe Coding? Will It Change How Programmers Work?
Technical trends, engineering practices, and an indie developer perspective on Vibe Coding, AI, and independent development.
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Complete Technology Stack Analysis for LLM Application Development
Covers tech trends, engineering practices, and indie developer perspectives around large models, RAG, and AI.
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.