JavaScript and TypeScript course Β· Module 6: TypeScript Basics

Union and intersection types

9 min read
In this lesson4

Welcome to the advanced TypeScript type laboratory in Jurassic Park! Dr. Henry Wu is especially excited today, because we will be discussing two powerful TypeScript mechanisms that can be compared to his most ambitious genetic engineering projects: union and intersection types.

Union Types - "either this or that"

A union type allows a variable to accept values of one of several different types. It's a bit like a hybrid enclosure in the park that can accommodate different species of dinosaurs, but only one kind at a time.

Basic syntax of union types

We create a union type using the | (pipe) operator:

1// A variable can be of type string OR number
2let identyfikatorDinosaura: string | number;
3
4identyfikatorDinosaura = "TREX-01"; // Ok, string is allowed
5identyfikatorDinosaura = 12345;     // Ok, number is allowed
6// identyfikatorDinosaura = true;   // Error! boolean is not part of the union string | number

Practical applications of union types

Union types are extremely useful in many practical scenarios in Jurassic Park:

1. Handling different identifier formats

1// Function that can accept different types of identifiers
2function findDinosaur(id: string | number): void {
3  // We must check the type before performing type-specific operations
4  if (typeof id === "string") {
5    console.log(`Looking for dinosaur with code: ${id.toUpperCase()}`);
6  } else {
7    console.log(`Looking for dinosaur with number: ${id.toFixed(0)}`);
8  }
9}
10
11findDinosaur("RAPTOR-BLUE"); // "Looking for dinosaur with code: RAPTOR-BLUE"
12findDinosaur(42);            // "Looking for dinosaur with number: 42"

2. Functions returning different types

1// Function that can return a dinosaur or an error message
2function pobierzDaneDinosaura(id: string):
3  { nazwa: string; species: string; wiek: number } | string {
4
5  // Simulating a database
6  const bazaDanych = new Map([
7    ["TRex1", { nazwa: "Rex", species: "Tyrannosaurus", wiek: 7 }],
8    ["Rap1", { nazwa: "Blue", species: "Velociraptor", wiek: 4 }]
9  ]);
10
11  const dinosaur = bazaDanych.get(id);
12
13  if (dinosaur) {
14    return dinosaur; // Returns dinosaur object
15  } else {
16    return `Dinosaur with ID not found: ${id}`; // Returns error message
17  }
18}
19
20const wynik = pobierzDaneDinosaura("TRex1");
21// We need to check the type of the returned value
22if (typeof wynik === "string") {
23  console.log(`Error: ${wynik}`);
24} else {
25  console.log(`Found: ${wynik.nazwa}, species: ${wynik.species}`);
26}

3. Handling optional parameters

1// Function generating a report that can accept different filter parameters
2function generateDinosaurReport(
3  filtr?: { species?: string; minAge?: number; predators?: boolean }
4): string {
5  let raport = "Dinosaur report:\n";
6
7  if (filtr) {
8    raport += "Filters:";
9    if (filtr.species) raport += ` Species=${filtr.species}`;
10    if (filtr.minAge) raport += ` MinAge=${filtr.minAge}`;
11    if (filtr.predators !== undefined) raport += ` OnlyPredators=${filtr.predators}`;
12  } else {
13    raport += "No filters - showing all dinosaurs";
14  }
15
16  return raport;
17}
18
19console.log(generateDinosaurReport()); // Without filters
20console.log(generateDinosaurReport({ species: "Velociraptor" })); // With species filter
21console.log(generateDinosaurReport({ minAge: 5, predators: true })); // With multiple filters

Discriminated Unions

Discriminated unions are an advanced technique of using union types with a "discriminating property" that allows TypeScript to recognize which type we're dealing with. It's like the dinosaur identification system in the park, which immediately tells whether we're dealing with a herbivore or a predator.

1// Defining interfaces with the discriminating property "typ"
2interface Herbivore {
3  typ: "herbivore";
4  dieta: string[];
5  poziomAgresji: number; // 1-10
6}
7
8interface Predator {
9  typ: "predator";
10  ofiary: string[];
11  biteForce: number; // in PSI
12}
13
14// Type that is a union of two interfaces
15type Dinosaur = Herbivore | Predator;
16
17// Function using the discriminated union
18function manageDinosaur(dino: Dinosaur): void {
19  // We check the discriminating property
20  switch (dino.typ) {
21    case "herbivore":
22      // TypeScript knows that dino is of type Herbivore here
23      console.log(`Prepare fodder: ${dino.dieta.join(", ")}`);
24      if (dino.poziomAgresji > 7) {
25        console.log("Warning: Herbivore with high aggression level!");
26      }
27      break;
28    case "predator":
29      // TypeScript knows that dino is of type Predator here
30      console.log(`Prepare meat for: ${dino.ofiary.join(", ")}`);
31      if (dino.biteForce > 10000) {
32        console.log("Warning: Very dangerous predator!");
33      }
34      break;
35  }
36}
37
38// Usage examples
39const triceratops: Herbivore = {
40  typ: "herbivore",
41  dieta: ["ferns", "cycads", "conifers"],
42  poziomAgresji: 6
43};
44
45const tRex: Predator = {
46  typ: "predator",
47  ofiary: ["triceratops", "edmontosaurus", "pachycephalosaurus"],
48  biteForce: 12800
49};
50
51manageDinosaur(triceratops);
52manageDinosaur(tRex);

Intersection Types - "both this and that"

An intersection type allows combining multiple types into one, more complex type. It's like Dr. Wu's experiments with combining DNA from different dinosaurs to create entirely new species with the traits of all "ingredients".

Basic syntax of intersection types

We create an intersection type using the & operator:

1// Defining interfaces of basic traits
2interface BasicDinosaur {
3  id: number;
4  nazwa: string;
5  species: string;
6  wiek: number;
7}
8
9interface PhysicalTraits {
10  height: number; // in meters
11  length: number; // in meters
12  waga: number;   // in kilograms
13}
14
15interface BehavioralTraits {
16  poziomAgresji: number; // 1-10
17  terytorialny: boolean;
18  social: boolean;
19}
20
21// Creating a type that is the intersection of three interfaces
22type FullDinosaurProfile = BasicDinosaur & PhysicalTraits & BehavioralTraits;
23
24// Now the object must implement all properties from all three interfaces
25const velociraptor: FullDinosaurProfile = {
26  // From BasicDinosaur
27  id: 101,
28  nazwa: "Blue",
29  species: "Velociraptor",
30  wiek: 4,
31
32  // From PhysicalTraits
33  height: 0.5,
34  length: 2,
35  waga: 15,
36
37  // From BehavioralTraits
38  poziomAgresji: 8,
39  terytorialny: true,
40  social: true
41};

Practical applications of intersection types

Intersection types are especially useful when building complex systems in Jurassic Park:

1. Composing behaviors

1// Defining different abilities
2interface Swimming {
3  swimmingSpeed: number; // km/h
4  maxDepth: number;    // meters
5  underwaterEndurance: number; // minutes
6}
7
8interface Flying {
9  wingspan: number; // meters
10  flightSpeed: number;       // km/h
11  maxAltitude: number;       // meters
12}
13
14// A dinosaur that can both swim and fly (like Pteranodon)
15type FlyingISwimmingDinosaur = BasicDinosaur & Flying & Swimming;
16
17const pteranodon: FlyingISwimmingDinosaur = {
18  id: 201,
19  name: "Pterri",
20  species: "Pteranodon",
21  age: 12,
22
23  // Flying abilities
24  wingspan: 8,
25  flightSpeed: 90,
26  maxAltitude: 500,
27
28  // Swimming abilities
29  swimmingSpeed: 20,
30  maxDepth: 10,
31  underwaterEndurance: 3
32};

2. Extending existing types

1// Basic security interface for all enclosures
2interface BasicSecuritySystem {
3  doubleFence: boolean;
4  videoMonitoring: boolean;
5  motionSensors: boolean;
6}
7
8// Additional security measures for predators
9interface PredatorSecurityMeasures {
10  electricVoltage: number; // volts
11  wallThickness: number; // cm
12  securityDome: boolean;
13}
14
15// Full security system for predator enclosure
16type PredatorEnclosure = BasicSecuritySystem & PredatorSecurityMeasures;
17
18// Using intersection to add enclosure information
19type TRexEnclosure = PredatorEnclosure & {
20  area: number; // hectares
21  terrainLayout: string[];
22  realisticVegetation: boolean;
23};
24
25const rexEnclosure: TRexEnclosure = {
26  // BasicSecuritySystem
27  doubleFence: true,
28  videoMonitoring: true,
29  motionSensors: true,
30
31  // PredatorSecurityMeasures
32  electricVoltage: 10000,
33  wallThickness: 50,
34  securityDome: false,
35
36  // Specific to TRexEnclosure
37  area: 8,
38  terrainLayout: ["tall grass", "hills", "stream"],
39  realisticVegetation: true
40};

3. Combining functionality in complex systems

1// Observation system interface
2interface ObservationSystem {
3  cameras: number;
4  resolution: string;
5  infraredTracking: boolean;
6
7  startRecording(): void;
8  stopRecording(): void;
9}
10
11// Alarm system interface
12interface AlarmSystem {
13  soundSignal: boolean;
14  lightSignal: boolean;
15  smsNotification: boolean;
16
17  triggerAlarm(level: number): void;
18  disableAlarm(): void;
19}
20
21// Complex security system combining both functionalities
22type ParkSecuritySystem = ObservationSystem & AlarmSystem;
23
24// Implementation of the security system
25class FullSecuritySystem implements ParkSecuritySystem {
26  // ObservationSystem
27  cameras: number = 24;
28  resolution: string = "4K";
29  infraredTracking: boolean = true;
30
31  startRecording(): void {
32    console.log("Starting recording from all cameras");
33  }
34
35  stopRecording(): void {
36    console.log("Stopping recording");
37  }
38
39  // AlarmSystem
40  soundSignal: boolean = true;
41  lightSignal: boolean = true;
42  smsNotification: boolean = true;
43
44  triggerAlarm(level: number): void {
45    console.log(`Activating level ${level} alarm!`);
46    if (this.soundSignal) console.log("Sirens activated");
47    if (this.lightSignal) console.log("Warning lights activated");
48    if (this.smsNotification) console.log("Sending SMS notifications to staff");
49  }
50
51  disableAlarm(): void {
52    console.log("Disabling all alarm systems");
53  }
54
55  // Additional methods specific to the full system
56  initiateSecurityProcedure(threatCode: string): void {
57    console.log(`Initiating security procedure: ${threatCode}`);
58    this.startRecording();
59    this.triggerAlarm(threatCode === "RED" ? 3 : 1);
60  }
61}
62
63// Using the system
64const securitySystem = new FullSecuritySystem();
65securitySystem.initiateSecurityProcedure("RED");

Combining union and intersection - advanced cases

The true power of TypeScript is revealed when we combine union and intersection types into more complex structures - just as Dr. Wu combines DNA from different species to create entirely new dinosaurs.

1// Basic dinosaur types
2interface Dinosaur {
3  id: number;
4  name: string;
5  species: string;
6}
7
8interface LandDinosaur {
9  environment: "land";
10  runSpeed: number; // km/h
11}
12
13interface WaterDinosaur {
14  environment: "water";
15  swimmingSpeed: number; // km/h
16  maxDepth: number; // meters
17}
18
19interface AirDinosaur {
20  environment: "air";
21  flightSpeed: number; // km/h
22  maxAltitude: number; // meters
23}
24
25// Types for different abilities
26interface HuntingAbilities {
27  biteForce: number; // PSI
28  huntingStrategy: "ambush" | "chase" | "pack";
29}
30
31interface HerbivoreDefensiveAbilities {
32  armor: boolean;
33  horns: boolean;
34  spikes: boolean;
35  defensiveStrength: number; // 1-10
36}
37
38// Complex type using union and intersection
39type ParkDinosaur = Dinosaur &
40  (
41    (LandDinosaur & HuntingAbilities) |
42    (LandDinosaur & HerbivoreDefensiveAbilities) |
43    (WaterDinosaur & HuntingAbilities) |
44    (AirDinosaur & HuntingAbilities)
45  );
46
47// Examples of use
48const tyrannosaurus: ParkDinosaur = {
49  id: 1,
50  name: "Rex",
51  species: "Tyrannosaurus Rex",
52  environment: "land",
53  runSpeed: 30,
54  biteForce: 12800,
55  huntingStrategy: "chase"
56};
57
58const stegosaurus: ParkDinosaur = {
59  id: 2,
60  name: "Spike",
61  species: "Stegosaurus",
62  environment: "land",
63  runSpeed: 7,
64  armor: true,
65  horns: false,
66  spikes: true,
67  defensiveStrength: 8
68};
69
70// Function handling complex types
71function analyzeDinosaur(dino: ParkDinosaur): void {
72  console.log(`Analyzing: ${dino.name} (${dino.species})`);
73
74  // Check the environment
75  if (dino.environment === "land") {
76    console.log(`Running speed: ${dino.runSpeed} km/h`);
77
78    // Check whether it's a predator or herbivore
79    if ("biteForce" in dino) {
80      console.log(`Predator - bite force: ${dino.biteForce} PSI`);
81      console.log(`Hunting strategy: ${dino.huntingStrategy}`);
82    } else if ("defensiveStrength" in dino) {
83      console.log("Herbivore with defensive mechanisms:");
84      if (dino.armor) console.log("- Has armor");
85      if (dino.horns) console.log("- Has horns");
86      if (dino.spikes) console.log("- Has spikes");
87      console.log(`Defensive strength: ${dino.defensiveStrength}/10`);
88    }
89  } else if (dino.environment === "water") {
90    console.log(`Aquatic predator - swimming speed: ${dino.swimmingSpeed} km/h`);
91    console.log(`Maximum depth: ${dino.maxDepth} m`);
92  } else if (dino.environment === "air") {
93    console.log(`Aerial predator - flying speed: ${dino.flightSpeed} km/h`);
94    console.log(`Maximum flight altitude: ${dino.maxAltitude} m`);
95  }
96}
97
98analyzeDinosaur(tyrannosaurus);
99console.log("---");
100analyzeDinosaur(stegosaurus);

Summary

Union and intersection types are like Dr. Wu's advanced genetic engineering techniques - they give us extraordinary power and flexibility in designing complex TypeScript systems.

TypeOperatorJurassic Park analogyUse
Union``Enclosure that can hold different dinosaurs (but only one kind at a time)Variables that can have different types
Discriminated Union`` with discriminating propertyDinosaur identification system for different speciesSafe handling of different type variants
Intersection&Combining DNA from different dinosaursCombining interfaces/types into more complex structures

Dr. Wu summarizes today's lesson: "TypeScript, like our genetics laboratory, gives us enormous possibilities. Thanks to union and intersection types we can create systems precisely tailored to our needs, combining simple elements into complex structures. However remember - with great power comes great responsibility. Use this knowledge wisely to create a safe and reliable park."

Code for this lesson: index.ts
1// Union and intersection types in Jurassic Park
2console.log("Union and Intersection Types\n");
3
4// ===========================================
5// 1. UNION TYPES - type A or B
6// ===========================================
7console.log("=== UNION TYPES ===");
8
9// A simple union type
10type DinosaurDiet = "carnivore" | "herbivore" | "omnivore";
11
12let trexDiet: DinosaurDiet = "carnivore";
13let brachiosaurDiet: DinosaurDiet = "herbivore";
14
15console.log("T-Rex diet:", trexDiet);
16console.log("Brachiosaurus diet:", brachiosaurDiet);
17
18// Union with different types
19type ID = number | string;
20
21let paddockId: ID = "P-01";
22let dinosaurId: ID = 123;
23
24console.log("Paddock ID:", paddockId);
25console.log("Dinosaur ID:", dinosaurId);
26
27// Union with objects
28interface Carnivore {
29  type: "carnivore";
30  huntingStyle: string;
31  preySize: string;
32}
33
34interface Herbivore {
35  type: "herbivore";
36  plantPreference: string;
37  grazingHours: number;
38}
39
40type Dinosaur = Carnivore | Herbivore;
41
42const trex: Carnivore = {
43  type: "carnivore",
44  huntingStyle: "ambush predator",
45  preySize: "large"
46};
47
48const triceratops: Herbivore = {
49  type: "herbivore",
50  plantPreference: "ferns and cycads",
51  grazingHours: 16
52};
53
54// Type guards for union types
55function describeDiet(dino: Dinosaur): string {
56  if (dino.type === "carnivore") {
57    return `Carnivore: ${dino.huntingStyle}, preys on ${dino.preySize} animals`;
58  } else {
59    return `Herbivore: prefers ${dino.plantPreference}, grazes ${dino.grazingHours}h/day`;
60  }
61}
62
63console.log("\nT-Rex:", describeDiet(trex));
64console.log("Triceratops:", describeDiet(triceratops));
65
66// ===========================================
67// 2. INTERSECTION TYPES - type A and B
68// ===========================================
69console.log("\n=== INTERSECTION TYPES ===");
70
71// Combining types
72interface HasName {
73  name: string;
74}
75
76interface HasWeight {
77  weight: number;
78}
79
80interface HasDiet {
81  diet: DinosaurDiet;
82}
83
84// Intersection - must have all properties
85type CompleteDinosaur = HasName & HasWeight & HasDiet;
86
87const velociraptor: CompleteDinosaur = {
88  name: "Blue",
89  weight: 350,
90  diet: "carnivore"
91};
92
93console.log("Complete dinosaur:", velociraptor);
94
95// Extending interfaces through intersection
96interface BasicInfo {
97  id: number;
98  species: string;
99}
100
101interface LocationInfo {
102  paddockId: string;
103  zone: string;
104}
105
106interface HealthInfo {
107  isHealthy: boolean;
108  lastCheckup: Date;
109}
110
111type FullDinosaurInfo = BasicInfo & LocationInfo & HealthInfo;
112
113const rexy: FullDinosaurInfo = {
114  id: 1,
115  species: "Tyrannosaurus Rex",
116  paddockId: "P-01",
117  zone: "Carnivore Zone",
118  isHealthy: true,
119  lastCheckup: new Date()
120};
121
122console.log("\nFull info:", rexy);
123
124// ===========================================
125// 3. Combining Union and Intersection
126// ===========================================
127console.log("\n=== UNION + INTERSECTION ===");
128
129interface Swimmer {
130  swimSpeed: number;
131}
132
133interface Runner {
134  runSpeed: number;
135}
136
137interface Flyer {
138  flySpeed: number;
139}
140
141// Dinosaur can swim OR fly, BUT must be able to run
142type ActiveDinosaur = Runner & (Swimmer | Flyer);
143
144const mosasaurus: ActiveDinosaur = {
145  runSpeed: 5, // slow on land
146  swimSpeed: 50 // fast in the water
147};
148
149const pteranodon: ActiveDinosaur = {
150  runSpeed: 10,
151  flySpeed: 80
152};
153
154console.log("Mosasaurus:", mosasaurus);
155console.log("Pteranodon:", pteranodon);
156
157// ===========================================
158// 4. Practical uses
159// ===========================================
160console.log("\n=== PRACTICAL USES ===");
161
162// API Response type
163type SuccessResponse = {
164  status: "success";
165  data: any;
166};
167
168type ErrorResponse = {
169  status: "error";
170  message: string;
171  code: number;
172};
173
174type ApiResponse = SuccessResponse | ErrorResponse;
175
176function handleResponse(response: ApiResponse): void {
177  if (response.status === "success") {
178    console.log("Data received:", response.data);
179  } else {
180    console.log(`Error ${response.code}: ${response.message}`);
181  }
182}
183
184const success: ApiResponse = {
185  status: "success",
186  data: { dinosaurs: [trex, triceratops] }
187};
188
189const error: ApiResponse = {
190  status: "error",
191  message: "Paddock breach detected",
192  code: 503
193};
194
195handleResponse(success);
196handleResponse(error);
197
198// Event types
199interface BaseEvent {
200  timestamp: Date;
201  source: string;
202}
203
204interface FeedingEvent extends BaseEvent {
205  type: "feeding";
206  dinosaurId: number;
207  foodAmount: number;
208}
209
210interface EscapeEvent extends BaseEvent {
211  type: "escape";
212  dinosaurId: number;
213  paddockId: string;
214  severity: "low" | "high";
215}
216
217interface MaintenanceEvent extends BaseEvent {
218  type: "maintenance";
219  paddockId: string;
220  duration: number;
221}
222
223type ParkEvent = FeedingEvent | EscapeEvent | MaintenanceEvent;
224
225function logEvent(event: ParkEvent): void {
226  console.log(`[${event.timestamp.toISOString()}] ${event.source}`);
227
228  switch (event.type) {
229    case "feeding":
230      console.log(`  Feeding dinosaur #${event.dinosaurId}: ${event.foodAmount}kg`);
231      break;
232    case "escape":
233      console.log(`  ESCAPE from paddock ${event.paddockId}! Severity: ${event.severity}`);
234      break;
235    case "maintenance":
236      console.log(`  Maintenance on paddock ${event.paddockId}: ${event.duration}h`);
237      break;
238  }
239}
240
241const feedingEvent: FeedingEvent = {
242  type: "feeding",
243  timestamp: new Date(),
244  source: "Automated System",
245  dinosaurId: 1,
246  foodAmount: 400
247};
248
249const escapeEvent: EscapeEvent = {
250  type: "escape",
251  timestamp: new Date(),
252  source: "Security Alert",
253  dinosaurId: 1,
254  paddockId: "P-01",
255  severity: "high"
256};
257
258console.log("\nEvent log:");
259logEvent(feedingEvent);
260logEvent(escapeEvent);
261
262// ===========================================
263// 5. Summary
264// ===========================================
265console.log("\n=== SUMMARY ===");
266console.log("Union Types (|) - value can be one of the types");
267console.log("Intersection Types (&) - value must satisfy all types");
268console.log("Type Guards - checking the type at runtime");
269console.log("Discriminated Unions - distinguishing by field");
270
271console.log("\nUnion and Intersection Types mastered!");

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the type string | number mean in TypeScript?

  2. 2. What does the type A & B (intersection type) do?

Hands-on tasks in the game

  • Code editor

    Create a union type DietType = 'herbivore' | 'carnivore' | 'omnivore' and an intersection type AquaticDino = Dinosaur & SwimmingAbility.

  • Horizontal ordering

    Arrange the elements of a type alias:

  • Click in order

    Arrange the elements of a union type definition:

  • Vertical ordering

    Arrange the steps of narrowing a union type in TypeScript:

Useful articles