Skip to main content

TS Ep 7: Union and Intersection Types — Set Theory Mechanics

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 7: This Article
In TypeScript, types are sets of possible values. Union types (|) represent the logical OR (Set Union), while Intersection types (&) represent the logical AND (Set Intersection). Mastering how these set operations interact with object properties is fundamental to advanced type design.

1. Types as Sets
#

To understand Unions and Intersections deeply, we must view TypeScript types through the lens of Set Theory:

  • The type string is the infinite set of all possible string values ("a", "hello", …).
  • The type number is the set of all floating-point numbers.
  • A Union Type (A | B) represents the Union of Set A and Set B ($A \cup B$). A value belongs to the union if it belongs to Set A OR Set B.
  • An Intersection Type (A & B) represents the Intersection of Set A and Set B ($A \cap B$). A value belongs to the intersection if it belongs to Set A AND Set B.
       UNION (A | B)                    INTERSECTION (A & B)
   +-------------------+                +-------------------+
   |  +---+     +---+  |                |     +---+-+---+   |
   |  | A |  OR | B |  |                |     | A |&| B |   |
   |  +---+     +---+  |                |     +---+-+---+   |
   +-------------------+                +-------------------+
     Accepts A or B                       Accepts values in
     (Larger Value Set)                   BOTH sets simultaneously

2. Union Types (|)
#

A Union Type specifies that a variable can hold a value matching any one of the specified types.

// ID can be a string OR a number
type Identifier = string | number;

let userId: Identifier = 1001;     // Valid
userId = "usr_0099";              // Valid
// userId = true;                 // ❌ Error: Type 'boolean' is not assignable to type 'Identifier'.

Property Access on Unions (The Intersection of Keys!)
#

A counter-intuitive aspect of set theory in TypeScript involves object properties on union types.

If you have a union of object types A | B, TypeScript will only allow accessing properties that exist on BOTH A and B without narrowing.

interface AdminUser {
  id: string;
  name: string;
  permissions: string[];
}

interface CustomerUser {
  id: string;
  name: string;
  creditCardToken: string;
}

type User = AdminUser | CustomerUser;

function processUser(user: User) {
  // 🟢 WORKED: 'id' and 'name' exist on BOTH AdminUser and CustomerUser
  console.log(user.id, user.name);

  // ❌ Compiler Error: Property 'permissions' does not exist on type 'User'.
  // Property 'permissions' does not exist on type 'CustomerUser'.
  // console.log(user.permissions); 
}
Important

The Key Paradox: The set of values for A | B is the Union of their values, but the set of safely accessible properties on A | B is the Intersection of their keys!


3. Intersection Types (&)
#

An Intersection Type combines multiple types into a single composite type.

interface HasID {
  id: string;
}

interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

// DatabaseEntity MUST have 'id', 'createdAt', AND 'updatedAt'
type DatabaseEntity = HasID & HasTimestamps;

const entity: DatabaseEntity = {
  id: "ent_9918",
  createdAt: new Date(),
  updatedAt: new Date(),
};

Primitive Intersections $\to$ never
#

If you intersect two mutually exclusive primitive types (which have no overlapping values), the resulting set is empty. In TypeScript, an empty type set evaluates to never:

type Impossible = string & number; // Evaluates to 'never'

// ❌ Impossible to assign anything to 'never'!
// const x: Impossible = "hello"; 

4. Property Collisions in Object Intersections
#

What happens if you intersect two interfaces that contain the same property name, but with different primitive types?

interface TypeA {
  id: string;
  value: string;
}

interface TypeB {
  id: string;
  value: number; // Conflicting property type!
}

type Merged = TypeA & TypeB;

In this scenario:

  • id is string & string, which resolves cleanly to string.
  • value is string & number, which resolves to never!
// The resulting shape of Merged is effectively:
// { id: string; value: never; }

const invalid: Merged = {
  id: "101",
  // ❌ Error: Type 'number' is not assignable to type 'never'.
  // value: 42, 
};

Because value evaluated to never, it is impossible to construct a valid object matching Merged.


5. Operator Precedence: & over |
#

Like mathematical multiplication (*) having higher precedence than addition (+), Intersection (&) has higher precedence than Union (|).

// Evaluates as: A | (B & C)
type Complex = string | number & boolean; 

To prevent confusion and enforce clear intent, always use parentheses when mixing unions and intersections:

type ClearIntent = (string | number) & boolean; // Resolves to 'never'
type GroupedUnion = string | (number & boolean); // Resolves to 'string'

6. Real-World Code Example: Pattern Combining & and |
#

interface BaseResponse {
  status: number;
  timestamp: number;
}

interface SuccessResponse extends BaseResponse {
  success: true;
  data: { id: string; name: string };
}

interface ErrorResponse extends BaseResponse {
  success: false;
  error: { code: string; message: string };
}

// Discriminated Union composed of Intersections
type ApiResponse = SuccessResponse | ErrorResponse;

function handleApiResponse(response: ApiResponse) {
  // Shared properties are directly accessible
  console.log(`HTTP Status: ${response.status}`);

  if (response.success) {
    // Narrowed to SuccessResponse
    console.log(`User Data: ${response.data.name}`);
  } else {
    // Narrowed to ErrorResponse
    console.error(`API Error: ${response.error.message}`);
  }
}

Summary & Next Steps
#

In this episode:

  • We analyzed types as mathematical sets ($A \cup B$ vs $A \cap B$).
  • We solved the Key Paradox: A | B allows values of either, but restricts property access to the intersection of shared keys.
  • We demonstrated how intersecting incompatible primitive properties generates never.
  • We reviewed operator precedence (& before |) and safe grouping.

In Episode 8: Literal Types, we will explore how specific string and number literals ("admin", 404) form exact unit types in TypeScript!

typescript - This article is part of a series.
Part 7: This Article