6.6 The Pitfalls of Intersecting Primitives
Alright, let’s talk about one of the first places TypeScript’s type system will gleefully smack you in the face: intersecting primitives. You’ve probably seen the & operator and thought, “Ah, union is OR, so intersection must be AND. I’ll take this string and this number and get a type that is both a string AND a number! Checkmate, type system!”
Let me stop you right there. I need you to imagine the TypeScript compiler, a profoundly logical but utterly humorless entity, staring at you blankly. It’s not going to create a new quantum value that is simultaneously a string and a number. That’s not how our universe works, and it’s certainly not how TypeScript’s type algebra works.
Instead, it calculates the intersection of the sets of values these types represent. What value belongs to both the set of all strings and the set of all numbers? Precisely none of them. The intersection of those two sets is empty. So, what do you get? The type never.
type Nonsense = string & number;
// type Nonsense = never
function proveIt(value: Nonsense) {
console.log(value);
}
// This is the ONLY way to call this function without cheating the type system.
proveIt("hello"); // Error: Argument of type 'string' is not assignable to parameter of type 'never'.
proveIt(42); // Error: Argument of type 'number' is not assignable to parameter of type 'never'.
proveIt(undefined); // Also an Error.
You’ve effectively created a function that can’t be called with any valid value. It’s a logical impossibility. Congratulations, you’ve type-safeified nothing. This is the type system equivalent of dividing by zero.
So When Does It Work?
Intersections are incredibly useful, but you have to use them on types that can actually overlap. Their superpower is mixing together object types.
Think of it as a “must-have-all-of-these” contract. You’re telling the compiler, “This value must have the properties from Type A and the properties from Type B.” It’s how you do composition and create rich types from simpler ones.
interface Employee {
name: string;
employeeId: number;
startDate: Date;
}
interface Manager {
department: string;
directReports: Employee[];
}
type ManagerEmployee = Employee & Manager;
const sarah: ManagerEmployee = {
name: "Sarah",
employeeId: 12345,
startDate: new Date("2020-01-01"),
department: "Engineering",
directReports: [] // she's a new manager, cut her some slack
};
// Sarah happily satisfies both interfaces.
This is the good stuff. We’ve built a new, more specific type without having to redefine all those properties. This is the intended use case.
The Subtle Trap with Union Primitives
Here’s a more insidious pitfall. What happens when you intersect types that might have some overlap?
type StringOrNumber = string | number;
type NumberOrBoolean = number | boolean;
type WhatAmI = StringOrNumber & NumberOrBoolean;
What’s the resulting type? Let’s apply the set logic again. A value for WhatAmI must be a member of both StringOrNumber and NumberOrBoolean.
- Is
stringin both? No, it’s not inNumberOrBoolean. - Is
booleanin both? No, it’s not inStringOrNumber. - Is
numberin both? Yes.
Therefore, WhatAmI is just number. The intersection finds the common ground between the two unions. This makes perfect logical sense, but it can be surprising if you’re just thinking of & as “combine stuff.”
The One Weird Exception: string Literals
There’s one fascinating, albeit niche, exception to the “primitives yield never” rule: string literal types. Because the literal "admin" is a more specific subset of the general type string, they can intersect.
type AdminString = "admin" & string; // type AdminString = "admin"
type AlsoAdmin = string & "admin"; // type AlsoAdmin = "admin"
This works because the set of values for "admin" is entirely contained within the set of values for string. Their intersection is just the more specific type, "admin". It feels a bit like saying “the intersection of ‘animals’ and ‘cats’ is ‘cats’.” It’s technically true, but you’d rarely write it this way on purpose. You’d just write "admin". The compiler does this kind of reduction automatically to keep types clean.
The Real-World Takeaway
The rule of thumb is simple: Stop trying to intersect broad primitives. The & operator is your tool for composing objects, not for performing alchemy on string, number, and boolean. If you find yourself writing string & number, it’s a massive red flag that your model is wrong. You probably meant to use a union (string | number) to allow either type, or you need to step back and design an object structure that properly represents your data.
Use intersections to combine facets of an object. Use unions to represent variability in values. Keep this distinction clear, and you’ll avoid an entire class of confusing and impossible types.