Skip to content

TypeScript Discriminated Unions for Document Signing States

Model document preparation, sending and completion as distinct states, then test the real transitions separately from static type checks.

GuideProductivity

By

Updated 3 min read
TypeScript Workflow States: document illustration with IndieTools branding

A discriminated union can make a document workflow easier to maintain by representing each meaningful state as a distinct shape. Begin with the information that must exist at each stage, rather than a collection of independent booleans such as sent, complete and failed that can form contradictory combinations.

Holosign, a signature product in the TypeScript collection, supplies a useful workflow context. The model below is an illustrative design exercise. A declared technology does not establish that the product uses this exact state representation internally.

Define states through their evidence

Consider a prepared document, a sending attempt, a document awaiting recipients and a completed record. For each state, write down the identifier, timestamp and supporting evidence the application needs. An unsuccessful send attempt may retain a retry reason while the document itself remains editable.

Decide whether an operational job state belongs on the same object as the business record. Combining them can make a temporary transport failure look like a permanent document outcome.

Use names that people operating the product can explain. A type that mirrors unclear interface language will preserve that ambiguity in code.

Make each branch carry relevant fields

The TypeScript narrowing guide describes unions whose members share a property with distinct literal values. Checking that property lets the compiler narrow the remaining fields. It also shows how the never type supports exhaustive handling.

For a completed-state branch, require the evidence your application actually needs to display completion. Avoid making every field optional across every state merely to silence compiler errors. That returns the burden to each caller and permits combinations the domain never intended.

Keep private fields out of a public display model even when both representations share an identifier. A convenient union should not accidentally become a full database export.

Test the transition rules separately

Static checking helps developers handle known shapes. It does not prove that a particular user is allowed to move a real record from one state to another. The server still needs authorisation, current-record checks and validation of external evidence.

Write transition tests around the saved state and the triggering event. Include a duplicate completion notification, a delayed earlier event and an attempt to modify a completed record. Specify whether each case is ignored, rejected or recorded as a separate event.

Preserve idempotency where retries are expected. Returning the same successful outcome for an already-applied event can be appropriate, provided the application verifies that it is genuinely the same event and record.

Review every consumer when adding a state

Adding a new union member should prompt review of the interface, notifications, exports, background jobs and support tools. Exhaustive handlers help reveal missing code paths, but persisted older records and independently deployed clients still require compatibility planning.

Choose a safe default for unknown external values rather than asserting them into the union. An unrecognised provider status should remain visible as an integration issue until its meaning is understood.

Keep the model close to the user outcome

Use a small scenario matrix with initial state, event, expected next state and visible message. Compare that matrix to the actual workflow after a change. A compiler pass is valuable evidence about source consistency; the scenario tests establish different facts about behaviour.

The related optional-field migration guide addresses another common modelling problem: distinguishing an omitted update from an explicit empty value. Clear field semantics and clear state semantics work together to prevent a polished interface from hiding an ambiguous mutation.

More guide articles