Use a union type when the code needs to act on several concrete entity types in one place, but those types do not naturally share behavior or structure. This keeps the design honest, avoids meaningless interfaces, and centralises the shared logic in one function. If runtime behaviour depends on the specific type, pass an explicit discriminator alongside the union.
Choosing a Union Type Instead of a Forced Base Interface
Teams should reach for a union type when several entities need the same handling flow but do not genuinely share a stable abstraction. The key benefit is honesty in the type system: you model what exists, not what would be convenient for inheritance. That matters because a fake base interface often hides differences that later become maintenance bugs, especially when one type grows a field or behaviour the others should not inherit. When the shared operation is narrow, a union keeps the design explicit and the call site readable.
In practice, this is also a governance choice for code quality. A union makes the common path visible while preserving type-specific constraints, so developers are less likely to smuggle unrelated entities into a shared contract just to satisfy a pattern. The result is usually less refactoring churn and fewer accidental assumptions about polymorphism. In practice, many engineering teams discover the need for a union only after a rushed base interface has already accumulated exceptions and type checks.
For teams that want a general control perspective on design discipline, the NIST SP 800-53 Rev 5 Security and Privacy Controls is useful here because it reinforces the value of explicit control boundaries rather than ambiguous shared assumptions.
How the Pattern Works in Real Code
A union type works best when the function needs one shared outcome, but the inputs remain distinct enough that a common base would be artificial. The function accepts all permitted variants, then narrows each case before applying the relevant branch of logic. That narrowing can be based on a discriminator field, a type guard, or another runtime check that is reliable enough to distinguish the concrete entities. The important point is that the shared handling belongs in one place, while the differences stay visible at the boundary.
This pattern is especially strong when the shared work is something like validation, formatting, enrichment, routing, or persistence preparation. It prevents the codebase from inventing a parent interface whose only real purpose is to satisfy one function. It also reduces the temptation to add placeholder methods or empty members that exist for inheritance rather than meaning. If the entities truly diverge in behaviour, a union lets each branch stay accurate without dragging the others into a misleading contract.
- Use a union when the shared operation is the same but the data model is not.
- Add an explicit discriminator when runtime decisions depend on the concrete type.
- Narrow early in the function so each branch remains easy to reason about.
- Prefer small shared handlers over a broad interface that only appears reusable.
The guidance breaks down when the entities start to accumulate enough common behaviour that a real abstraction emerges, because at that point the union becomes a workaround rather than the clearest model.
When a Union Is the Wrong Abstraction
Tighter typing often increases branching, so teams need to balance honesty against the overhead of repeated case handling. A union is a good fit when the similarity is operational, not structural, but it becomes less attractive when the types start converging around real shared behaviour. At that point, forcing everything through one function can obscure reuse and make the code harder to evolve.
The main edge case is a domain where the entities share enough lifecycle, validation, or processing rules that a common interface is no longer artificial. In those situations, a union can still work, but only if the branches remain genuinely distinct and the function is still the right place for the shared logic. Another edge case is a public API: if consumers need a stable contract across versions, a union may be easier to extend carefully than a base interface that promises methods some types cannot support. The consensus view is that unions are best for narrow shared handling; there is no universal rule that they should replace interfaces in every case.
When the types differ in ways that affect downstream behaviour, the safest design is to keep the union explicit rather than burying the differences behind inheritance.
Standards & Framework Alignment
This section maps relevant standards and security frameworks to the operational risks and controls described in this guidance.
CIS Controls v8 and NIST CSF 2.0 set the governance and control requirements practitioners need to meet.
| Framework | Control / Reference | Relevance |
|---|---|---|
| CIS Controls v8 | 16 — Application Software Security | Union types improve code clarity and reduce faulty abstraction in application logic. |
| Recommendation — Use secure design review to keep shared handling explicit and avoid misleading inheritance. | ||
| NIST CSF 2.0 | ID.AM-2 — Hardware and software platforms are inventoried | Clear type modelling supports accurate software understanding and maintainability. |
| PR.DS-5 — Protect data at rest | Explicit branching helps prevent accidental misuse of different data-bearing entities. | |
| Recommendation — Document application components and interfaces so shared logic stays visible and traceable. Apply validation boundaries that keep entity-specific handling from leaking across types. | ||
Practitioner Guidance
What to prioritise: Check whether the shared logic is truly the same for every variant. If the answer is yes, a union is usually cleaner than a thin base interface that only exists to unify the call site.
Decision rule: If the only commonality is “these objects are processed together,” keep the model explicit with a union and, where needed, a discriminator. If you find yourself adding abstract methods or dummy members, that is a sign the abstraction is drifting away from the domain.
What practitioners underestimate: The real cost is not the union itself but the future branches it creates. Teams should be deliberate about where that branching belongs, because pushing it into multiple callers often creates more inconsistency than a single well-structured handler.
Practitioner takeaway: Use the union to preserve domain truth, and move to a shared interface only when the types really do share behaviour, not just a processing convenience.
Related resources from NHI Mgmt Group
- How should security teams respond when cybercrime and cyberwarfare use the same TTPs?
- How should security teams govern AI use when the same model creates different risk in different contexts?
- Should healthcare teams use the same zero trust model for AI agents and service accounts?
- Should teams use the same controls for human, service, and agent MCP identities?