# Union and intersection types

Source: https://codewiki.com/typescript/union-intersection/

> - **what**: A union type `A | B` accepts a value satisfying at least one member; an intersection type `A & B` requires a value to satisfy both.
> - **trap**: `|` doesn't put every property on one object, and `&` doesn't merge objects at runtime. Conflicting properties can also make an intersection impossible to construct normally.
> - **fix**: Narrow a union before using member-specific fields, give object unions a stable discriminant, and inspect overlapping properties before forming an intersection.

## What it is and why it exists

A union type represents alternatives. A `string | number` value may be a string or a number, and a caller may supply either. Until the receiver identifies the member, it can perform only operations that are safe for every possible value.

An intersection type represents requirements that must all hold. `Identified & Timestamped` requires one value to have both identity and time fields. It composes independent capabilities and object fragments without requiring an inheritance relationship between their declarations.

The two operators solve different static-modeling problems: a union describes a choice, while an intersection accumulates constraints. Function inputs, parse results, state machines, and event payloads often use unions; permissions, metadata, and reusable capabilities often use intersections.

TypeScript decides compatibility through structural typing. A value can satisfy several declarations at once when it has the required structure, so a union is an inclusive “or,” not an exclusive choice.

Don't treat unions and intersections as runtime containers. They are erased during compilation: they create no tag, copy no property, and validate no external data. Type declarations must agree with real object construction and runtime checks.

## How it works

For ordinary types, it helps to approximate a type as a set of possible values. `A | B` contains values belonging to `A` or `B`, so it is usually wider; `A & B` contains only values belonging to both, so it is usually narrower. This model explains assignment direction and `never`, although special types such as `any` break the simple set intuition.

An expression of type `A` can be assigned to `A | B` because the union accepts more possibilities. The reverse usually fails because an `A | B` value might satisfy only `B`. An `A & B` expression can be assigned separately to `A` and `B` because it already meets both requirements.

An object union doesn't automatically become an object with every field optional. `{ email: string } | { phone: string }` means that at least one structure matches, and one runtime object may match both. Use a discriminated union to preserve relationships between fields instead of making every field optional.

Intersections aren't limited to objects. `string & "ready"` narrows to the literal type `"ready"`, while `string & number` has no ordinary value and becomes `never`. Object intersections are simply their most common use in a structural type system.

### Safe operations on a union

When a variable is `string | number`, the compiler must account for both members. Both have `toString()`, so it can be called directly. Only strings have `toUpperCase()` and only numbers have `toFixed()`, so those operations require narrowing first.

A “common member” means a property and use that the checker can prove safe, not merely a repeated property name. If two members have functions with the same name but incompatible parameter contracts, knowing that the property exists doesn't make a call safe.

The following table summarizes both compositions from a consumer's point of view:

| Composition | Value must satisfy | Information available before narrowing | Common uses |
| --- | --- | --- | --- |
| `A | B` | At least one of `A`, `B` | Information safe for every remaining member | Input choices, states, results |
| `A & B` | All of `A`, `B` | Combined compatible members from both sides | Capability composition, added metadata |

### Narrowing and discriminated unions

Narrowing removes union members using runtime facts. `typeof`, `instanceof`, equality, `in`, and custom type predicates supply different evidence. A check must reflect the runtime value honestly; a type assertion supplies no evidence.

An object union works best when every member shares one literal discriminant. Both `status: "paid"` and `status: "failed"` exist at runtime and let the compiler narrow the whole object together with its payload. This pattern is a discriminated union.

Once every member has been handled, the remaining value has type `never`, the bottom type. Passing a default branch's value to a function that accepts `never` turns a future unhandled member into a compile error.

This exhaustiveness guarantee covers only a union the compiler already trusts. JSON, message queues, and JavaScript callers can supply arbitrary values, so external data must still be validated from `unknown` before entering a discriminated union.

### Intersections, overlapping properties, and runtime values

An object intersection accumulates constraints on one value. If `A` requires `id: string` and `B` requires `createdAt: Date`, then `A & B` requires both fields. The type alias itself doesn't produce an object containing them.

Compatible overlapping properties narrow further. For example, `{ mode: string } & { mode: "safe" }` has `mode: "safe"`. Incompatible overlapping properties intersect to `never`, as in `{ id: string } & { id: number }`.

A property conflict doesn't use object spread order to choose the “last value.” JavaScript object spread does overwrite repeated names, but that is a runtime evaluation rule. An intersection requires the final value to satisfy both static constraints. Confusing the two often leads to an unsafe `as A & B`.

Inspect overlapping keys and their meanings before composing third-party types. If the domain truly replaces a field, remove the old constraint with `Omit` before adding the new one. If two fields mean different things, rename them instead of asserting over the conflict.

## Examples

The next four programs cover a primitive union, a discriminated union, an object intersection, and a union with shared intersection context. Each was checked in `strict` mode with TypeScript 6.0.3 and actually executed with local `tsx`.

### Narrowing a union

An identifier can arrive as a numeric database key or an external string. The function identifies the member with `typeof` before calling methods specific to that member.

<!-- quick -->

```typescript
// file: identifier.ts
type Identifier = string | number;

function canonicalId(id: Identifier): string {
  if (typeof id === "number") {
    return `customer:${id.toString().padStart(4, "0")}`;
  }

  return `customer:${id.trim().toLowerCase()}`;
}

const identifiers: Identifier[] = [42, "  ALPHA-7  "];

for (const identifier of identifiers) {
  console.log(canonicalId(identifier));
}
```

```text
customer:0042
customer:alpha-7
```


<!-- /quick -->

The numeric branch can call `padStart()` because `toString()` has already produced a string. The string branch needs no assertion: after the previous branch returns, only `string` remains in the control flow.

This union says that two input representations are accepted; it doesn't decide whether both forms can identify the same customer. Deduplication, ranges, and empty strings are domain rules that the function or an outer validator must still handle explicitly.

### Keeping state and payload correlated with a discriminant

Each payment state carries its own required payload. After the `switch` checks `status`, it can access the corresponding field directly, while the default branch proves that the current union is exhausted.

```typescript
// file: payment-state.ts
type PaymentState =
  | { status: "pending"; attempt: number }
  | { status: "paid"; receipt: string }
  | { status: "failed"; reason: string };

function assertNever(value: never): never {
  throw new Error(`Unhandled payment: ${JSON.stringify(value)}`);
}

function summarize(state: PaymentState): string {
  switch (state.status) {
    case "pending":
      return `pending attempt ${state.attempt}`;
    case "paid":
      return `paid with ${state.receipt}`;
    case "failed":
      return `failed: ${state.reason}`;
    default:
      return assertNever(state);
  }
}

const states: PaymentState[] = [
  { status: "pending", attempt: 2 },
  { status: "paid", receipt: "rcpt-81" },
  { status: "failed", reason: "card expired" },
];

for (const state of states) {
  console.log(summarize(state));
}
```

```text
pending attempt 2
paid with rcpt-81
failed: card expired
```

If you add `{ status: "refunded"; reference: string }` without adding a `case`, `state` in the default branch is no longer `never`. TypeScript reports an error at `assertNever(state)`.

Changing the model to `{ status: "pending" | "paid" | "failed"; attempt?: number; receipt?: string; reason?: string }` loses the correlation. That type permits `paid` without a `receipt` and permits contradictory fields on the same value.

### Combining independent capabilities with an intersection

A subscriber satisfies both the identity and preference contracts. A function receiving `Subscriber` can safely use fields from both sides because the caller must provide the complete intersection.

```typescript
// file: subscriber.ts
type HasIdentity = {
  id: string;
  email: string;
};

type HasPreferences = {
  locale: "en" | "zh";
  digest: boolean;
};

type Subscriber = HasIdentity & HasPreferences;

function deliveryLabel(subscriber: Subscriber): string {
  const schedule = subscriber.digest ? "daily" : "off";
  return `${subscriber.id}:${subscriber.locale}:${schedule}`;
}

const subscriber: Subscriber = {
  id: "acct-17",
  email: "reader@example.com",
  locale: "zh",
  digest: true,
};

console.log(deliveryLabel(subscriber));
console.log(Object.keys(subscriber).sort().join(","));
```

```text
acct-17:zh:daily
digest,email,id,locale
```

The second output line comes from the actual object's keys, not from the `Subscriber` type. Removing `email` from the object literal causes a compile error; removing the type alias doesn't change any runtime object.

These contracts have no overlapping keys, so the composition is direct. If both sides declare `locale` in a real project, first confirm that their types and meanings agree, then choose whether to keep the intersection, rename the fields, or reshape the object explicitly.

### Adding shared context to every union member

An import result still preserves two payloads through a discriminant, but every member must also carry `batchId`. The parentheses form the union first, and the outer intersection applies batch context to all its members.

```typescript
// file: import-result.ts
type ImportResult = (
  | { kind: "accepted"; records: number }
  | { kind: "rejected"; errors: string[] }
) & { batchId: string };

function report(result: ImportResult): string {
  if (result.kind === "accepted") {
    return `${result.batchId}:accepted:${result.records}`;
  }

  return `${result.batchId}:rejected:${result.errors.join("|")}`;
}

const results: ImportResult[] = [
  { kind: "accepted", records: 18, batchId: "batch-4" },
  {
    kind: "rejected",
    errors: ["missing email", "invalid locale"],
    batchId: "batch-5",
  },
];

for (const result of results) {
  console.log(report(result));
}
```

```text
batch-4:accepted:18
batch-5:rejected:missing email|invalid locale
```

`batchId` is available before narrowing because every path through the union has that intersection constraint. `records` and `errors` remain member-specific and become available only after checking `kind`.

The parentheses state the grouping and make the shared context's scope visible to a maintainer. If the common part grows, give it a name instead of copying the same fields into every union member.

## Pitfalls

### Treating a union as a property collection

> **Pitfall:** When a value is `EmailContact | PhoneContact`, reading `email` or `phone` directly isn't safe. The union says that either member may appear; it doesn't promise that every value has every member's fields.

**Fix:** add a stable literal discriminant and narrow first. If both fields must always exist, the model needs an intersection or one complete object type, not a union.

### Simulating distinct states with optional fields

> **Pitfall:** Making `data`, `error`, and `progress` all optional permits invalid combinations such as success without data or failure with data. A non-null assertion only hides the outcome; it doesn't restore field correlation.

**Fix:** define one complete member per state and distinguish them with the same literal field. Use `?` only when a field is genuinely optional within that member.

### Assuming `&` merges runtime objects

> **Pitfall:** `type Combined = A & B` only declares constraints. Asserting an object that has only `A` fields to `Combined` creates no `B` fields, so reading them can still produce `undefined`.

**Fix:** create the real value through an explicit object literal, reviewed spread, or constructor, then let the compiler check the result. Don't use `as A & B` as an object-merge operation.

### Ignoring overlapping intersection conflicts

> **Pitfall:** `{ id: string } & { id: number }` requires one `id` to satisfy two incompatible types, so the resulting field is `never`. An assertion may hide the error briefly, but it spreads an impossible contract to consumers.

**Fix:** review the overlap of `keyof` before composing types. When replacing a field, use `Omit<Old, "id"> & NewId` to state the intention and add tests for the runtime conversion.

### Swallowing new members in a generic default branch

> **Pitfall:** Returning `"unknown"` directly from the end of a `switch` lets a new union member fall silently into old behavior. The code still compiles even though the state machine is incomplete.

**Fix:** assign the remainder to `never` or pass it to `assertNever()`. Validate unknown external values before they enter the union instead of mixing them into static state handling through a default branch.

### Asserting a union at an external boundary

> **Pitfall:** `JSON.parse(raw) as PaymentState` skips checks of the container, discriminant, and payload. A statically exhaustive `switch` may still receive an arbitrary `status` at runtime.

**Fix:** put the parse result into `unknown` immediately, verify a non-null object, the exact discriminant, and the required fields for that member, then return the domain union. Give parse failures and unknown discriminants explicit outcomes.

<!-- deep -->

## Type algebra and reduction

Unions and intersections absorb duplicate members: both `T | T` and `T & T` are equivalent to `T`. Member order normally doesn't change meaning, so `A | B` and `B | A`, and `A & B` and `B & A`, describe the same value sets even when an editor displays them differently.

The compiler can reduce a composition when one member fully contains another. `string | "ready"` is `string` because the literal is already a string; `string & "ready"` is `"ready"` because it is the part satisfying both sides.

Because `never` has no ordinary runtime value, `T | never` is `T` and `T & never` is `never`. Because `unknown` can safely receive any value, `T | unknown` is `unknown` and `T & unknown` is `T`. These identities often appear in intermediate conditional and utility types.

The table collects these relationships. It describes static type reduction and performs no operation in JavaScript.

| Expression | Reduced result | Reason |
| --- | --- | --- |
| `T \| never` | `T` | Adds no possible value |
| `T & never` | `never` | No value can satisfy the empty set and another constraint |
| `T \| unknown` | `unknown` | The result accepts any unknown value |
| `T & unknown` | `T` | `unknown` adds no concrete requirement |
| `string & "ready"` | `"ready"` | The literal is already a subset of strings |

At the value-set level, intersection distributes over union, so `A & (B | C)` can be understood as `(A & B) | (A & C)`. This explains a common way to add the same metadata to every event member: write the event union first, then intersect it with `{ requestId: string }`.

Don't force the compiler to expand every composition just to make its display attractive. Naming intermediate concepts, retaining discriminants, and testing public assignability relationships are more stable than depending on how an editor prints an alias.

These algebraic rules don't prove runtime data trustworthy. Even when a complex type reduces correctly, a network value hasn't been checked. The static set model applies only to evidence that has entered the type system.

### Object unions preserve field relationships

The important value of an object union isn't collecting keys; it is preserving relationships between fields. `{ format: "json"; payload: string } | { format: "binary"; payload: Uint8Array }` says that `format` determines the type of `payload`.

Changing it to `{ format: "json" | "binary"; payload: string | Uint8Array }` makes the two unions vary independently. The type then permits `format: "json"` with a binary payload, and checking `format` can no longer narrow `payload`.

Union members can share ordinary fields, such as `requestId: string` on every result. A shared field is directly readable, while member-specific fields still require narrowing. When the common part is large, name it and intersect it with each member.

A union isn't exclusive-or. An object with enough structure may satisfy several members at once, so don't rely on the assumption that it “belongs to only one interface.” Distinct literal discriminants establish a real mutual exclusion.

## Designing usable intersections

Intersections work best for orthogonal constraints that can be explained independently. Identity, audit information, and pagination metadata can compose because each part has a clear owner. Types that both try to define core state are more likely to create naming and semantic conflicts.

Interface inheritance and intersections can both combine object requirements, but they fail differently. Interface inheritance rejects incompatible overlapping properties at the declaration. An intersection can form first and expose a conflicting property as `never` when used. For a stable public object hierarchy, the interface's earlier failure is often clearer; for local type operations, an intersection is more flexible.

Using `Omit` before an intersection explicitly replaces a field, but it remains static modeling. If the old value's field must change from a string to a number, runtime code must perform the conversion and handle failure; changing only the alias does nothing.

A long intersection often exposes an ownership problem. If one value must satisfy many unrelated capabilities, check whether a function is accepting a broader contract than it uses. Narrowing the parameter type is usually easier to test than adding another `&`.

### Keys and call signatures

For object unions, `keyof (A | B)` keeps only keys that are safely present on every member; for object intersections, `keyof (A & B)` normally contains keys from both sides. This matches property access: a union consumer has only common guarantees, while an intersection consumer has both guarantees.

The types of overlapping properties must also account for read and write directions. Two declarations allowing a key to be read doesn't mean any value accepted by either side can safely be written to it. Mutable objects, optional properties, and index signatures can defeat a simple “key merge” explanation.

An intersection of function types often behaves like a set of call signatures, and library declarations use it to describe a callable value that accepts several input shapes. The implementation must genuinely handle every signature; asserting a single-input function doesn't generate overloaded behavior.

When a public API needs several clear call forms, function overloads are usually easier to read than a handwritten function intersection. Intersections better suit results derived by utility types, provided positive and negative type tests cover the call contract.

### Static and runtime boundaries

Unions and intersections constrain only expressions checked by TypeScript. Values from JSON, DOM attributes, environment variables, or untyped packages must remain `unknown` until real runtime checks establish membership.

To validate a union, first check the container, then the discriminant, and finally the corresponding member's payload. Merely finding a `status` property is insufficient: arrays, wrong field types, and inherited properties can still pass a loose check.

To validate an intersection, verify every condition promised by both sides. Combining two incomplete predicates with `&&` doesn't make either implementation more truthful. The compiler trusts type-predicate signatures, so tests must include missing and conflicting fields.

Keep runtime assertions and compile-time type cases together in tests. Runtime tests prove parsing and construction behavior; type tests prove which combinations are accepted and rejected. Neither side replaces the other.

<!-- /deep -->

[Checkpoint: typescript/union-intersection](https://codewiki.com/typescript/union-intersection/#checkpoint)

## Further reading

- [TypeScript Handbook: Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html)
- [TypeScript Handbook: Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html)
- [TypeScript Handbook: Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)
- [TypeScript Handbook: Discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)
