|) 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
stringis the infinite set of all possible string values ("a","hello", …). - The type
numberis 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 simultaneously2. 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);
}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:
idisstring & string, which resolves cleanly tostring.valueisstring & number, which resolves tonever!
// 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 | Ballows 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!

