One of the most effective ways to understand TypeScript’s type system is to think of types not as rigid rules, but as sets of possible values. This mental model makes concepts like union (|) and intersection (&) much easier to grasp, especially when working with both primitive types and object types.
Types as Sets of Values
In TypeScript, every type can be thought of as a set of possible values.
string→ the set of all string values:"hello","world","","123", …number→ the set of all numbers:1,0,-5,3.14, …boolean→{ true, false }
Even any and never fit this mental model:
anyis the universal set (all possible values).neveris the empty set (no possible values).
Subsets and Supersets of Types
Just like in mathematics, some types are subsets of others.
Example with literals:
type Yes = "yes";
type YesOrNo = "yes" | "no";
"yes"is a subset of"yes" | "no"."yes" | "no"is a superset of"yes".
👉 You can assign a subset to a superset, but not vice versa:
let a: YesOrNo = "yes"; // ✅ OK
let b: Yes = "yes"; // ✅ OK
// b = "no"; // ❌ Error: "no" not assignable to "yes"
Union Types (X | Y) as Union of Sets
The union operator (|) combines two sets of values into one bigger set.
Example with primitives:
type StringOrNumber = string | number;
This is the union of all strings and all numbers.
So valid values are: "hello", 42, "world", 3.14, …
👉 You can visualize it as:
string = { "a", "b", "c", ... }
number = { 1, 2, 3, ... }
string | number = { "a", "b", "c", ... } ∪ { 1, 2, 3, ... }
Example with objects:
type Cat = { meow: () => void };
type Dog = { bark: () => void };
type CatOrDog = Cat | Dog;
- A
CatOrDogcan be a cat, or a dog. - If you only know it’s a
CatOrDog, you can’t assume both methods exist — you must narrow first.
function speak(pet: CatOrDog) {
if ("meow" in pet) pet.meow();
if ("bark" in pet) pet.bark();
}
Intersection Types (X & Y) as Intersection of Sets
The intersection operator (&) represents the overlap of sets — values that belong to both types at the same time.
Example with primitives:
type A = string;
type B = number;
type Impossible = A & B; // ❌ never
Since no value can be both a string and a number at once, the result is never (the empty set).
Example with objects:
type Person = { name: string };
type Worker = { company: string };
type Employee = Person & Worker;
Here, the intersection type means:
Employee = Person ∩ Worker = { name: string, company: string }
So an Employee must have both properties:
let e: Employee = { name: "Alice", company: "TechCorp" }; // ✅
Visualizing Union and Intersection
Think of Venn diagrams:
- Union (
|) = everything in circle X plus everything in circle Y. - Intersection (
&) = only the overlapping part of circle X and Y.
Practical Example: Combining Concepts
type ID = number | string; // union
type User = { id: ID; name: string };
type Admin = { id: ID; role: "admin" };
type AdminUser = User & Admin; // intersection
IDis a union of strings and numbers.AdminUseris an intersection ofUserandAdmin.
So a valid AdminUser must contain all properties:
let admin: AdminUser = {
id: "123",
name: "Alice",
role: "admin",
};
Conclusion
Thinking about TypeScript types as sets gives you a powerful framework:
- Every type = set of possible values.
- Subsets and supersets explain assignability.
- Union (
X | Y) = combination of sets. - Intersection (
X & Y) = overlap of sets. - never = empty set, any = universal set.
With this mental model, TypeScript’s type system becomes much less mysterious and much more intuitive — a natural extension of set theory applied to programming.