TypeScript 1.0 and the boundary I actually needed

TypeScript’s 1.0 release tempted a rewrite, but X8B6 gained more by typing operation and receipt boundaries before converting every line of JavaScript.

TypeScript reached 1.0 while X8B6 was acquiring a protocol: local operations, server receipts, projections, and several varieties of uncertain state. I expected the new language to help and immediately imagined converting the whole project.

That was the least useful place to begin.

The dangerous mistakes were concentrated at boundaries. An operation could lose its identity. A receipt could omit a revision. A source payload could call a quantity a string on one path and a number on another. A UI component could treat pending as a boolean while the storage layer expected a status word.

I introduced TypeScript at those seams first. The goal was not to make the repository typed. It was to make the offline protocol harder to misstate.

The first type described a promise

X8B6's operation object existed in several handwritten forms. The composer created one shape, IndexedDB stored another, the sender serialized a third, and tests used minimal fixtures that omitted inconvenient fields.

I began with the accepted operation contract:

interface InventoryOperation {
  operationId: string;
  itemId: string;
  kind: string;
  createdAt: string;
  baseRevision: number;
  payload: OperationPayload;
}

TypeScript 1.0 could express interfaces, classes, generics, modules, and ordinary annotations while compiling to JavaScript the existing browsers could run. It did not require the server or every dependency to be rewritten before one file became useful.

The first interface was intentionally conservative. Later refinements would make kind and payload relationships stronger, but even this version caught missing identities, numeric revisions passed as strings, and fixtures that never represented a storable operation.

The type was not merely a developer convenience. It documented the fields required for safe retry. A compile error at that boundary pointed toward a broken protocol promise.

Valid JavaScript gave migration room

TypeScript's relationship to JavaScript mattered more than novelty. Existing JavaScript syntax could move into .ts files incrementally, then gain annotations where the compiler needed help.

I converted a small module and kept its emitted JavaScript readable. The build targeted the JavaScript level supported by the project's browsers. I did not require a new runtime or ship type metadata to clients. Types guided development and disappeared from the output.

This made rollback plausible. If the experiment complicated the build beyond its value, the generated program was still ordinary JavaScript and the architectural boundary remained useful.

Gradual adoption did not mean every JavaScript file was automatically safe. Untyped values entering a typed module could still carry anything at runtime. It meant the conversion could follow risk instead of file order.

I prioritized operation normalization, receipt parsing, storage records, and projection transitions. CSS helpers and small DOM effects could wait. The migration map became a map of system boundaries rather than a campaign to change extensions.

That reduced churn and produced useful feedback early.

X8B6 passed plain objects between browser layers. I did not want every payload wrapped in a class merely to satisfy a type system.

TypeScript checked compatibility by structure. An object with the required operation fields could satisfy the interface without inheriting from a base class or carrying runtime tags. That fit JSON-shaped data and incremental migration.

The benefit came with a warning. Structural compatibility could allow two concepts with the same primitive shape to be mixed. itemId and operationId were both strings. The compiler could not infer their different meanings from field values passed separately.

I kept important concepts inside named objects rather than passing long lists of strings and numbers. When a function truly accepted an operation identity, its parameter name and surrounding interface carried the distinction.

I considered elaborate wrapper classes for every identifier and rejected them for this stage. They added construction and serialization complexity while the more common failures were missing fields and unchecked boundary payloads.

Structural typing worked well when the structures represented meaningful contracts. It was not a substitute for domain vocabulary.

Incoming JSON was still untrusted

One of my first typed functions asserted that a parsed response was an OperationReceipt:

var receipt = <OperationReceipt>JSON.parse(responseText);

The assertion convinced the compiler and validated nothing at runtime. A response without operationId would still enter the program. Type annotations described what code expected; they did not transform external data into truth.

I introduced explicit normalization functions at untyped boundaries:

function parseReceipt(value: any): OperationReceipt {
  if (!value || typeof value.operationId !== 'string') {
    throw new Error('Receipt is missing operation identity');
  }
  if (typeof value.confirmedRevision !== 'number') {
    throw new Error('Receipt revision is not numeric');
  }
  return value;
}

The 2014 implementation was more manual than later schema tools would make it. Its position was the important part. Network responses, IndexedDB records, query values, and configuration entered through runtime checks. Inside that boundary, TypeScript could reason from a validated shape.

This kept the type system from laundering arbitrary JSON. A cast was not evidence.

X8B6 depended on JavaScript libraries that knew nothing about TypeScript. Ambient declarations could describe the surface the project used without changing the library implementation.

I started with deliberately small .d.ts files rather than attempting to model every method. The IndexedDB helper declaration covered opening the store, starting a transaction, and reading or writing operations. The DOM and language libraries already provided broader declarations through the compiler environment.

The local declaration acted as an adapter contract. If project code called an undeclared convenience method, either the declaration was incomplete or the call relied on an unsupported behavior. Both deserved review.

An inaccurate declaration was dangerous. The compiler would trust it. I wrote a few integration tests against the real library and kept the declaration next to the adapter that owned it. Updating the dependency required checking both runtime behavior and declared surface.

The goal was not maximum type coverage of third-party code. It was a narrow fence: this is the part of the untyped library X8B6 depends on, and this is what the adapter promises to return.

That approach made old JavaScript dependencies compatible with a typed core without pretending they had become typed internally.

The storage boundary exposed optionality

Browser storage records did not always contain every field. Older schema versions lacked baseRevision; a pending operation might have no receipt; a corrupted record could have a partial payload.

The initial type made fields required because the current code wanted them. That caused me to cast migration inputs until they looked current, hiding the exact uncertainty migration needed to handle.

I separated stored versions:

interface StoredOperationV1 {
  operationId: string;
  itemId: string;
  delta: number;
}
 
interface StoredOperationV2 {
  schemaVersion: number;
  operation: InventoryOperation;
  localStatus: string;
}

The migration function accepted the old shape and either produced a validated current record or a recoverable error. Current application code did not carry every historical optional field forever.

This was a better use of types than adding question marks until compilation passed. Optionality belonged where the domain allowed absence. Historical variation belonged at the migration boundary.

The compiler made it inconvenient to forget that an old record could lack a base revision. That inconvenience protected unsent work.

One status string was still too loose

The first localStatus: string annotation prevented numbers and allowed every misspelling. confirmed, comfirmed, and done were all strings.

In the 1.0-era code, I represented the finite states through an enum or a set of named constants plus a constrained interface, depending on the emitted shape I wanted. The important change was making transitions consume and return a known state rather than arbitrary labels.

enum LocalOperationStatus {
  Pending,
  Sending,
  Confirmed,
  NeedsReview
}

The runtime values were less descriptive in storage than string words, so persistence translated between stable serialized names and internal enum values. I did not write raw enum ordinals into durable records, where reordering members could change meaning.

This boundary revealed another lesson: a convenient compile-time representation is not automatically a good storage protocol. Serialized forms needed stable, explicit values and runtime validation.

Typing transitions caught impossible calls, but the transition function still enforced rules such as “confirmed cannot return to sending.” A finite type listed states; it did not define their allowed graph.

Generics helped the receipt keep its result

Different operations returned different result details. An adjustment receipt included resulting quantity. A move receipt included both location balances. My first interface used result: any, creating an untyped hole at the moment code most needed to interpret the outcome.

Generics let the common receipt envelope retain a specific result shape:

interface OperationReceipt<TResult> {
  operationId: string;
  outcome: ReceiptOutcome;
  confirmedRevision: number;
  result: TResult;
}

The adjustment path worked with OperationReceipt<AdjustmentResult>. The move path used OperationReceipt<MoveResult>. Shared storage and identity code could operate on the envelope without discarding result information.

I avoided building a deeply generic operation framework. The project's operation types were few, and concrete functions remained easier to read. The generic captured one real relationship: a receipt envelope preserves the type of its result.

This was the kind of abstraction TypeScript made tempting. I adopted it only where the relationship reduced duplication without hiding domain verbs.

The measure was not whether the type looked elegant. It was whether a wrong result could cross a boundary unnoticed.

Modules clarified ownership before bundling did

The JavaScript project used global namespaces and script order. TypeScript's module features offered ways to group declarations and control exported surfaces, though the project's browser build constraints shaped which form was practical.

I used modules to separate protocol definitions, storage adapters, projection transitions, and UI presentation. Only narrow constructors and functions were exported. Helpers for mutating local indexes stayed private.

This reduced accidental coupling even when the generated JavaScript still joined an existing build pipeline. A view module could import a read-only projection shape without gaining direct access to the database adapter.

I did not reorganize every file around theoretical layers. The module boundaries followed ownership discovered during offline work:

protocol      operation and receipt contracts
storage       validated durable records and transactions
projection    deterministic state transitions
transport     request and retry behavior
presentation  view models and user intents

The compiler could then report when presentation reached into a storage implementation detail. The architecture became partly executable.

Modules were useful not because globals were unfashionable, but because offline correctness depended on keeping responsibilities from editing one another's state.

The first broad conversion produced a wall of errors. Many came from old DOM assumptions and incomplete library declarations, far from the protocol work I wanted to protect. A perpetually red build teaches people to ignore the compiler.

I tightened one boundary at a time and kept the converted subset compiling. Where an untyped module remained, an adapter explicitly returned any or a narrow declared surface. The unsafe edge was visible rather than dispersed through casts.

I treated every new cast as a review point. Sometimes a cast was necessary to bridge a library limitation. Often it meant runtime validation or a better interface was missing.

The compiler joined the normal build instead of becoming an optional command. A type error blocked generation of the candidate bundle in development. I still ran runtime tests because successful compilation did not prove IndexedDB transactions, network timing, or projection policy.

Fast, credible feedback mattered more than a high percentage typed. Ten dependable errors were more valuable than a thousand warnings everyone learned to bypass.

This kept adoption connected to trust rather than ceremony.

Output was part of the performance budget

TypeScript compiled to JavaScript, and the output still had to fit the phone and browser constraints that C62Y had exposed. Language features could emit helper patterns or larger class structures depending on target and code style.

I inspected the generated JavaScript and compared the minified bundle. Types themselves disappeared, but architecture choices still affected runtime size and initialization. A rewrite into many classes was not free merely because interfaces emitted nothing.

The compiler target matched the browsers X8B6 supported. I did not write code assuming a newer runtime simply because TypeScript syntax accepted it. Where the compiler could transform a feature appropriately, I verified the result. Where a runtime API was absent, a type declaration could not create it.

This distinction prevented another form of type confidence. The compiler could know Promise in a later environment only if the runtime actually supplied the behavior; in the 1.0 project I kept asynchronous boundaries in the callbacks and small abstractions the target supported rather than importing a future architecture into the retrospective.

Adoption had to improve correctness without quietly breaking reach or page cost.

Types did not replace scenario tests

The type system caught a receipt paired with the wrong result shape. It could not prove that a timeout reused the same operation identity, that a transaction committed receipt and projection together, or that a recount conflict displayed both intentions.

Those behaviors stayed in tests built around timelines and storage fixtures. TypeScript made the fixtures more complete by requiring valid operation shapes. The simulator still controlled failures and ordering.

One test deliberately fed malformed JSON through the runtime parser. Typed internal calls could never generate that input, which was precisely why the boundary test mattered. Another loaded a version-one storage record and checked the migration outcome.

I separated assurances:

compiler: internal shape compatibility
runtime parser: external value validation
unit tests: transition and policy examples
integration tests: storage and transport behavior
scenario tests: interruption and ordering

No layer claimed the work of the others. Their overlap at the operation boundary made failures easier to locate.

The rewrite would have optimized the wrong metric

A repository-wide conversion would have produced an impressive diff and delayed the protocol. Many errors would describe presentation utilities while the unsafe network and storage casts remained under active change.

Boundary-first adoption produced a smaller visible milestone: operations, stored records, receipts, and projections shared one vocabulary. That vocabulary then pulled nearby files into TypeScript naturally. A UI module converted when it benefited from the typed view model. A utility stayed JavaScript until its ownership changed.

The gradual path also revealed which types were stable enough to publish internally. Had I typed the mutable record model first, I would have created excellent tooling around the wrong abstraction.

Waiting until the operation model clarified prevented the compiler from hardening accidental shapes. Type systems amplify a model; they do not choose it.

The best migration order followed the cost of misunderstanding, not the directory tree.

What 1.0 changed for the project

TypeScript 1.0 gave X8B6 a practical, incremental way to make JavaScript contracts explicit while continuing to emit ordinary JavaScript. Interfaces described plain data structurally. Generics preserved receipt result relationships. Declaration files fenced untyped libraries. Modules clarified ownership. The compiler caught incompatible internal assumptions before they became offline failures.

It also taught limits early. JSON still needed runtime validation. Durable serialized values needed stable formats. Types could not create missing browser APIs, decide conflict policy, or prove asynchronous ownership. Incorrect declarations could make the compiler confidently wrong.

The adoption succeeded because it began where a shape carried a product promise. An operation identity could not disappear between composer and outbox. A receipt could not casually lose its revision. A stored old version could not masquerade as current without migration.

More files became TypeScript over time, but file count was never the breakthrough. The breakthrough was turning a few dangerous handoffs into named, reviewable contracts while the system was still small enough to understand them.

The restraint mattered historically as well as technically. TypeScript was newly at 1.0, its ecosystem was younger, and declarations for every dependency could not be assumed. A narrow adoption let the project benefit from the language without pretending its surrounding tools had already reached their later maturity.

I arrived expecting a safer rewrite. I left with a better boundary.

That boundary made later adoption easier because it began with a reason stronger than enthusiasm: an offline action needed one meaning everywhere it travelled.