JavaScript and TypeScript course Β· Module 7: Advanced TypeScript
Conditional Types
In this lesson8
"Dr. Wu, we have a situation," says a nervous lab assistant. "The DNA pattern we're trying to sequence could be from either a predatory or herbivorous dinosaur species, but we need different processing protocols depending on which one we're dealing with."
Dr. Henry Wu, Jurassic Park's chief geneticist, responds calmly: "That's not a problem. Our sequencing algorithms are intelligent enough to detect predation markers in DNA and automatically apply the appropriate protocol. That's the flexibility of our system."
Just like the advanced DNA sequencing algorithms in Jurassic Park, TypeScript has a mechanism called conditional types that allows dynamically determining types based on other types. This tool enables creating more flexible and precise APIs that adapt to different scenarios.
Conditional Type Basics
Conditional types in TypeScript work similarly to conditional expressions in JavaScript, but at the type level. Their syntax resembles the ternary operator:
1T extends U ? X : YThis should be read as: "If type T extends (is compatible with) type U, then the result is type X, otherwise type Y".
Simple Example
Let's create a simple function that processes an object representing a dinosaur and returns different results depending on its diet:
1type Carnivore = { type: "carnivore"; huntingSkill: number };
2type Herbivore = { type: "herbivore"; foragingSkill: number };
3
4type Dinosaur = Carnivore | Herbivore;
5
6// Conditional type determining the appropriate result type
7type DinosaurActivity<T extends Dinosaur> = T extends Carnivore
8 ? { activity: "hunting"; preyTargeted: string }
9 : { activity: "grazing"; plantsConsumed: number };
10
11// Function using the conditional type
12function trackDinosaurActivity<T extends Dinosaur>(dino: T): DinosaurActivity<T> {
13 if (dino.type === "carnivore") {
14 // TypeScript knows that for Carnivore we should return { activity: "hunting"; preyTargeted: string }
15 return {
16 activity: "hunting",
17 preyTargeted: "Gallimimus"
18 } as DinosaurActivity<T>;
19 } else {
20 // TypeScript knows that for Herbivore we should return { activity: "grazing"; plantsConsumed: number }
21 return {
22 activity: "grazing",
23 plantsConsumed: 42
24 } as DinosaurActivity<T>;
25 }
26}
27
28// Example usage
29const trex: Carnivore = { type: "carnivore", huntingSkill: 10 };
30const triceratops: Herbivore = { type: "herbivore", foragingSkill: 8 };
31
32const trexActivity = trackDinosaurActivity(trex);
33console.log(trexActivity.activity); // "hunting"
34console.log(trexActivity.preyTargeted); // "Gallimimus"
35
36const triceratopsActivity = trackDinosaurActivity(triceratops);
37console.log(triceratopsActivity.activity); // "grazing"
38console.log(triceratopsActivity.plantsConsumed); // 42In this example, the type DinosaurActivity<T> changes depending on whether T extends Carnivore or not. This allows us to return different data structures, and TypeScript provides full type support for both cases.
Distributive Conditional Types
One of the key features of conditional types is their distributive behavior with union types. When we apply a conditional type to a union type, the condition is applied to each element of the union separately.
1type ToArray<T> = T extends any ? T[] : never;
2
3// With a simple type, the result is simple
4type NumberArray = ToArray<number>; // number[]
5
6// With a union type, the condition is applied distributively
7type NumberOrStringArray = ToArray<number | string>; // number[] | string[]In the second case, ToArray<number | string> is equivalent to ToArray<number> | ToArray<string>, which gives number[] | string[], not (number | string)[].
We can apply this to our dinosaur example:
1// Defining more dinosaur types
2type TRex = { species: "tyrannosaurus"; strength: number };
3type Velociraptor = { species: "velociraptor"; speed: number };
4type Triceratops = { species: "triceratops"; armorLevel: number };
5type Brachiosaurus = { species: "brachiosaurus"; height: number };
6
7// Conditional type filtering predators
8type IsPredator<T> = T extends { species: "tyrannosaurus" | "velociraptor" } ? T : never;
9
10// Distributive application on union types
11type Predators = IsPredator<TRex | Velociraptor | Triceratops | Brachiosaurus>;
12// Wynik: TRex | Velociraptor
13
14// We can also filter herbivores
15type IsHerbivore<T> = T extends { species: "triceratops" | "brachiosaurus" } ? T : never;
16type Herbivores = IsHerbivore<TRex | Velociraptor | Triceratops | Brachiosaurus>;
17// Wynik: Triceratops | BrachiosaurusInferred Types
TypeScript allows extracting (inferring) types from other types using the infer keyword. This is particularly useful in conditional types.
1// Type that extracts the return type of a function
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4// Example usage
5function studyDinosaur(specimen: string): { species: string; age: number } {
6 return {
7 species: "Velociraptor",
8 age: 5
9 };
10}
11
12// Extracted return type of the studyDinosaur function
13type StudyResult = ReturnType<typeof studyDinosaur>; // { species: string; age: number }In this example, infer R tells TypeScript to "deduce" the return type of the function and assign it to type R, which is then used as the result of the conditional type.
We can create more complex type extraction examples, for instance:
1// Type representing dinosaur DNA
2type DinosaurDNA = {
3 species: string;
4 genomeSequence: string;
5 markers: {
6 predatory?: boolean;
7 aquatic?: boolean;
8 flying?: boolean;
9 size: "small" | "medium" | "large";
10 };
11};
12
13// Extracting markers from DNA
14type ExtractMarkers<T> = T extends { markers: infer M } ? M : never;
15
16// Extracting size from markers
17type ExtractSize<T> = T extends { size: infer S } ? S : never;
18
19// Example usage
20type DinosaurMarkers = ExtractMarkers<DinosaurDNA>;
21// { predatory?: boolean; aquatic?: boolean; flying?: boolean; size: "small" | "medium" | "large"; }
22
23type DinosaurSize = ExtractSize<DinosaurMarkers>;
24// "small" | "medium" | "large"Practical Applications of Conditional Types
Conditional types are especially useful in advanced programming scenarios, such as:
1. Creating Flexible API Components
1// Example API for dinosaur operations
2interface DinosaurOperations<T> {
3 create: (data: T) => Promise<T>;
4 update: (id: string, data: Partial<T>) => Promise<T>;
5 delete: (id: string) => Promise<void>;
6
7 // Method that returns different types depending on parameter T
8 analyze: T extends { genomeSequence: string }
9 ? (id: string) => Promise<{ compatibilityScore: number }>
10 : (id: string) => Promise<{ behaviorPrediction: string }>;
11}
12
13// Implementation for dinosaurs with genome sequence
14interface GenomeSequencedDino {
15 id: string;
16 name: string;
17 genomeSequence: string;
18}
19
20// Implementation for dinosaurs without genome sequence
21interface BehaviorStudiedDino {
22 id: string;
23 name: string;
24 observedBehaviors: string[];
25}
26
27// Different implementations for different types
28function createDinosaurAPI<T extends GenomeSequencedDino | BehaviorStudiedDino>(
29 baseUrl: string
30): DinosaurOperations<T> {
31 return {
32 create: (data: T) => fetch(`${baseUrl}/create`, {
33 method: 'POST',
34 body: JSON.stringify(data)
35 }).then(res => res.json()),
36
37 update: (id: string, data: Partial<T>) => fetch(`${baseUrl}/${id}`, {
38 method: 'PUT',
39 body: JSON.stringify(data)
40 }).then(res => res.json()),
41
42 delete: (id: string) => fetch(`${baseUrl}/${id}`, {
43 method: 'DELETE'
44 }).then(() => undefined),
45
46 // The analyze method implementation will differ depending on type T
47 analyze: ((id: string) => {
48 if ('genomeSequence' in { ...({} as T) }) {
49 return fetch(`${baseUrl}/${id}/genome-analysis`)
50 .then(res => res.json());
51 } else {
52 return fetch(`${baseUrl}/${id}/behavior-analysis`)
53 .then(res => res.json());
54 }
55 }) as any // Cast needed due to TypeScript runtime limitations
56 };
57}
58
59// Using different APIs
60const genomeAPI = createDinosaurAPI<GenomeSequencedDino>('/api/genome-dinos');
61const result1 = await genomeAPI.analyze('dino-1'); // { compatibilityScore: number }
62
63const behaviorAPI = createDinosaurAPI<BehaviorStudiedDino>('/api/behavior-dinos');
64const result2 = await behaviorAPI.analyze('dino-2'); // { behaviorPrediction: string }2. Implementing Type Patterns
1// Helper type Exclude - excludes types from a union
2type Exclude<T, U> = T extends U ? never : T;
3
4// Example: excluding dangerous dinosaurs
5type DangerousDinosaurs = "Tyrannosaurus" | "Velociraptor" | "Spinosaurus";
6type AllDinosaurs = "Tyrannosaurus" | "Triceratops" | "Velociraptor" | "Brachiosaurus" | "Stegosaurus" | "Spinosaurus";
7
8type SafeDinosaurs = Exclude<AllDinosaurs, DangerousDinosaurs>;
9// "Triceratops" | "Brachiosaurus" | "Stegosaurus"
10
11// Helper type Extract - extracts types meeting a condition
12type Extract<T, U> = T extends U ? T : never;
13
14// Example: extracting dinosaurs from the Jurassic era
15type JurassicDinosaurs = "Stegosaurus" | "Brachiosaurus" | "Allosaurus";
16type CretaceousDinosaurs = "Tyrannosaurus" | "Triceratops" | "Velociraptor";
17type AllEras = JurassicDinosaurs | CretaceousDinosaurs;
18
19type OnlyJurassic = Extract<AllEras, JurassicDinosaurs>;
20// "Stegosaurus" | "Brachiosaurus" | "Allosaurus"
21
22// Helper type NonNullable - removes null and undefined from a type
23type NonNullable<T> = T extends null | undefined ? never : T;
24
25// Example: ensuring we have non-empty dinosaur data
26type PossibleDinoData = { name: string; weight: number } | null | undefined;
27type DefiniteDinoData = NonNullable<PossibleDinoData>;
28// { name: string; weight: number }3. Creating Type Mappers
1// Type Mapper transforming object properties from one type to another
2type MapProperties<T, U> = {
3 [K in keyof T]: T[K] extends object ? MapProperties<T[K], U> : U;
4};
5
6// Example: mapping numeric values to strings
7type DinosaurStats = {
8 name: string;
9 physicalTraits: {
10 height: number;
11 weight: number;
12 speed: number;
13 };
14 behavioralTraits: {
15 aggression: number;
16 intelligence: number;
17 packBehavior: number;
18 };
19};
20
21// Transforming all numbers to strings for display purposes
22type DinosaurStatsForDisplay = MapProperties<DinosaurStats, string>;
23/*
24{
25 name: string;
26 physicalTraits: {
27 height: string;
28 weight: string;
29 speed: string;
30 };
31 behavioralTraits: {
32 aggression: string;
33 intelligence: string;
34 packBehavior: string;
35 };
36}
37*/Advanced Example: Type System for Jurassic Park Infrastructure
Let's look at a more complex example that uses conditional types to model different systems in Jurassic Park:
1// --- Basic system types ---
2
3type SystemStatus = "online" | "offline" | "maintenance" | "error";
4
5interface BaseSystem {
6 id: string;
7 name: string;
8 status: SystemStatus;
9 lastUpdated: Date;
10}
11
12// Different types of systems in the park
13interface SecuritySystem extends BaseSystem {
14 type: "security";
15 enclosures: string[];
16 alertLevel: "low" | "medium" | "high" | "critical";
17 cameras: number;
18 motionSensors: number;
19}
20
21interface PowerSystem extends BaseSystem {
22 type: "power";
23 outputCapacity: number; // kW
24 currentLoad: number; // kW
25 backupCapacity: number; // kW
26 generators: number;
27}
28
29interface TransportSystem extends BaseSystem {
30 type: "transport";
31 vehicles: number;
32 routes: string[];
33 passengerCapacity: number;
34 automatedGuidance: boolean;
35}
36
37interface LifeSupportSystem extends BaseSystem {
38 type: "lifesupport";
39 habitats: string[];
40 environmentalControls: {
41 temperature: number; // Β°C
42 humidity: number; // %
43 oxygen: number; // %
44 };
45}
46
47// Union of all system types
48type ParkSystem = SecuritySystem | PowerSystem | TransportSystem | LifeSupportSystem;
49
50// --- Conditional types for monitoring data ---
51
52/**
53 * Conditional type that determines what monitoring data is relevant for each system type
54 */
55type MonitoringData<T extends ParkSystem> = T extends SecuritySystem
56 ? {
57 breachDetections: number;
58 securityFootage: { cameraId: string; timestamp: Date; events: string[] }[];
59 activeAlarms: string[];
60 }
61 : T extends PowerSystem
62 ? {
63 loadDistribution: { sector: string; load: number }[];
64 energyEfficiency: number; // %
65 estimatedBackupTime: number; // hours
66 powerFluctuations: boolean;
67 }
68 : T extends TransportSystem
69 ? {
70 activeVehicles: number;
71 passengerCount: number;
72 delayReports: { routeId: string; delay: number }[];
73 fuelConsumption: number; // liters/hour
74 }
75 : T extends LifeSupportSystem
76 ? {
77 habitatConditions: {
78 habitatId: string;
79 temperature: number;
80 humidity: number;
81 oxygenLevel: number;
82 co2Level: number;
83 }[];
84 anomalyDetections: string[];
85 resourceConsumption: { water: number; electricity: number; air: number };
86 }
87 : never; // Fallback dla nieznanych types
88
89/**
90 * Conditional type determining recommended actions for systems in different states
91 */
92type RecommendedActions<T extends ParkSystem> =
93 T extends { status: "error" }
94 ? { severity: "high"; actions: string[]; escalation: string; timeToResolve: number }
95 : T extends { status: "maintenance" }
96 ? { checklistItems: string[]; estimatedCompletionTime: Date; personnelAssigned: string[] }
97 : T extends { status: "offline" }
98 ? { restartProcedures: string[]; systemDependencies: string[]; estimatedDowntime: number }
99 : { routineChecks: string[]; nextMaintenanceDate: Date };
100
101// --- System monitoring class using conditional types ---
102
103/**
104 * Class managing system monitoring, using conditional types
105 * to ensure type correctness for different systems
106 */
107class SystemMonitor<T extends ParkSystem> {
108 private system: T;
109 private monitoringData: MonitoringData<T>;
110 private recommendedActions: RecommendedActions<T>;
111
112 constructor(system: T) {
113 this.system = system;
114
115 // Initializing monitoringData based on system type
116 this.monitoringData = this.initializeMonitoringData();
117
118 // Initializing recommendedActions based on system type and status
119 this.recommendedActions = this.initializeRecommendedActions();
120 }
121
122 // Helper method for initializing monitoring data
123 private initializeMonitoringData(): MonitoringData<T> {
124 // Implementation dependent on type T
125 if (this.system.type === "security") {
126 return {
127 breachDetections: 0,
128 securityFootage: [],
129 activeAlarms: []
130 } as MonitoringData<T>;
131 } else if (this.system.type === "power") {
132 return {
133 loadDistribution: [],
134 energyEfficiency: 95,
135 estimatedBackupTime: 48,
136 powerFluctuations: false
137 } as MonitoringData<T>;
138 } else if (this.system.type === "transport") {
139 return {
140 activeVehicles: 0,
141 passengerCount: 0,
142 delayReports: [],
143 fuelConsumption: 0
144 } as MonitoringData<T>;
145 } else if (this.system.type === "lifesupport") {
146 return {
147 habitatConditions: [],
148 anomalyDetections: [],
149 resourceConsumption: { water: 0, electricity: 0, air: 0 }
150 } as MonitoringData<T>;
151 } else {
152 throw new Error(`Nieznany typ systemu: ${(this.system as any).type}`);
153 }
154 }
155
156 // Helper method for initializing recommended actions
157 private initializeRecommendedActions(): RecommendedActions<T> {
158 if (this.system.status === "error") {
159 return {
160 severity: "high",
161 actions: ["Natychmiastowa interwencja techniczna", "Powiadomienie kierownictwa"],
162 escalation: "Dyrektor ds. technicznych",
163 timeToResolve: 60 // minut
164 } as RecommendedActions<T>;
165 } else if (this.system.status === "maintenance") {
166 return {
167 checklistItems: ["Sprawdzenie okablowania", "Test functionality"],
168 estimatedCompletionTime: new Date(Date.now() + 3600000), // +1 godzina
169 personnelAssigned: ["Tech-1", "Tech-2"]
170 } as RecommendedActions<T>;
171 } else if (this.system.status === "offline") {
172 return {
173 restartProcedures: ["Kontrola zasilania", "Reset systemu"],
174 systemDependencies: ["Main zasilanie"],
175 estimatedDowntime: 120 // minut
176 } as RecommendedActions<T>;
177 } else {
178 return {
179 routineChecks: ["Codzienny overview", "Sprawdzenie logs"],
180 nextMaintenanceDate: new Date(Date.now() + 7 * 24 * 3600000) // +7 dni
181 } as RecommendedActions<T>;
182 }
183 }
184
185 // Method for updating monitoring data
186 updateMonitoringData(newData: Partial<MonitoringData<T>>): void {
187 this.monitoringData = { ...this.monitoringData, ...newData };
188 console.log(`Zaktualizowano dane monitorowania dla ${this.system.name} (ID: ${this.system.id})`);
189 }
190
191 // Method for generating system report
192 generateSystemReport(): string {
193 const report = `
194RAPORT SYSTEMU: ${this.system.name.toUpperCase()} (ID: ${this.system.id})
195=========================================================
196Typ: ${this.system.type}
197Status: ${this.system.status}
198Last update: ${this.system.lastUpdated.toLocaleString()}
199
200DETAILS MONITOROWANIA:
201${this.formatMonitoringData()}
202
203ZALECANE ACTIONS:
204${this.formatRecommendedActions()}
205=========================================================
206Raport wygenerowany: ${new Date().toLocaleString()}
207`;
208
209 return report;
210 }
211
212 // Method for formatting monitoring data depending on system type
213 private formatMonitoringData(): string {
214 if (this.system.type === "security") {
215 const data = this.monitoringData as MonitoringData<SecuritySystem>;
216 return `
217Wykryte naruszenia: ${data.breachDetections}
218Aktywne alarmy: ${data.activeAlarms.length > 0 ? data.activeAlarms.join(", ") : "Brak"}
219Nagrania security: ${data.securityFootage.length}
220`;
221 } else if (this.system.type === "power") {
222 const data = this.monitoringData as MonitoringData<PowerSystem>;
223 return `
224Efficiency energetyczna: ${data.energyEfficiency}%
225Szacowany czas na zasilaniu awaryjnym: ${data.estimatedBackupTime} godzin
226Fluktuacje mocy: ${data.powerFluctuations ? "TAK" : "NIE"}
227Schedule loads: ${data.loadDistribution.map(d => `${d.sector}: ${d.load}kW`).join(", ")}
228`;
229 } else if (this.system.type === "transport") {
230 const data = this.monitoringData as MonitoringData<TransportSystem>;
231 return `
232Aktywne pojazdy: ${data.activeVehicles}
233Liczba passengers: ${data.passengerCount}
234Usage paliwa: ${data.fuelConsumption} liters/godz.
235Raporty delays: ${data.delayReports.length > 0
236 ? data.delayReports.map(d => `${d.routeId}: ${d.delay}min`).join(", ")
237 : "Brak delays"}
238`;
239 } else if (this.system.type === "lifesupport") {
240 const data = this.monitoringData as MonitoringData<LifeSupportSystem>;
241 return `
242Warunki w habitatach: ${data.habitatConditions.map(h =>
243 `${h.habitatId} (Temp: ${h.temperature}Β°C, Wilg: ${h.humidity}%, O2: ${h.oxygenLevel}%)`).join("\n")}
244Wykryte anomalie: ${data.anomalyDetections.length > 0 ? data.anomalyDetections.join(", ") : "Brak"}
245Usage resources: Woda: ${data.resourceConsumption.water}mΒ³, Power: ${data.resourceConsumption.electricity}kWh, Powietrze: ${data.resourceConsumption.air}mΒ³
246`;
247 } else {
248 return `Brak danych monitorowania dla tego typu systemu.`;
249 }
250 }
251
252 // Method for formatting recommended actions depending on system status
253 private formatRecommendedActions(): string {
254 if (this.system.status === "error") {
255 const actions = this.recommendedActions as RecommendedActions<{ status: "error" }>;
256 return `
257Poziom validity: ${actions.severity}
258Zalecane actions: ${actions.actions.join(", ")}
259Eskalacja do: ${actions.escalation}
260Szacowany czas naprawy: ${actions.timeToResolve} minut
261`;
262 } else if (this.system.status === "maintenance") {
263 const actions = this.recommendedActions as RecommendedActions<{ status: "maintenance" }>;
264 return `
265Elementy do sprawdzenia: ${actions.checklistItems.join(", ")}
266Szacowany czas endings: ${actions.estimatedCompletionTime.toLocaleString()}
267Przydzielony personel: ${actions.personnelAssigned.join(", ")}
268`;
269 } else if (this.system.status === "offline") {
270 const actions = this.recommendedActions as RecommendedActions<{ status: "offline" }>;
271 return `
272Procedury restartu: ${actions.restartProcedures.join(", ")}
273Dependencies systemowe: ${actions.systemDependencies.join(", ")}
274Szacowany czas przestoju: ${actions.estimatedDowntime} minut
275`;
276 } else {
277 const actions = this.recommendedActions as RecommendedActions<ParkSystem>;
278 return `
279Rutynowe kontrole: ${actions.routineChecks.join(", ")}
280Next overview: ${actions.nextMaintenanceDate.toLocaleDateString()}
281`;
282 }
283 }
284
285 // Method returning recommended actions
286 getRecommendedActions(): RecommendedActions<T> {
287 return this.recommendedActions;
288 }
289
290 // Method for updating system status
291 updateSystemStatus(newStatus: SystemStatus): void {
292 (this.system as any).status = newStatus;
293 this.system.lastUpdated = new Date();
294
295 // Updating recommended actions after status change
296 this.recommendedActions = this.initializeRecommendedActions();
297
298 console.log(`Zaktualizowano status systemu ${this.system.name} na ${newStatus}`);
299 }
300}
301
302// --- Example usage ---
303
304// Creating sample systems
305const securitySystem: SecuritySystem = {
306 id: "SEC-001",
307 name: "MainMale system security",
308 status: "online",
309 lastUpdated: new Date(),
310 type: "security",
311 enclosures: ["T-Rex", "Velociraptor", "Dilophosaurus"],
312 alertLevel: "low",
313 cameras: 24,
314 motionSensors: 36
315};
316
317const powerSystem: PowerSystem = {
318 id: "PWR-001",
319 name: "MainMale system zasilania",
320 status: "online",
321 lastUpdated: new Date(),
322 type: "power",
323 outputCapacity: 10000,
324 currentLoad: 6500,
325 backupCapacity: 5000,
326 generators: 4
327};
328
329// Creating monitors for different systems
330const securityMonitor = new SystemMonitor<SecuritySystem>(securitySystem);
331const powerMonitor = new SystemMonitor<PowerSystem>(powerSystem);
332
333// Updating monitoring data
334securityMonitor.updateMonitoringData({
335 breachDetections: 3,
336 activeAlarms: ["Sektor 7G", "Northern ogrodzenie"],
337 securityFootage: [
338 { cameraId: "CAM-12", timestamp: new Date(), events: ["Ruch w areaze restricted"] }
339 ]
340});
341
342powerMonitor.updateMonitoringData({
343 loadDistribution: [
344 { sector: "Visitor Center", load: 2000 },
345 { sector: "TRex Paddock", load: 1500 },
346 { sector: "Raptor Enclosure", load: 1800 },
347 { sector: "Labs", load: 1200 }
348 ],
349 energyEfficiency: 92,
350 powerFluctuations: true
351});
352
353// Simulating a power system failure
354powerMonitor.updateSystemStatus("error");
355
356// Generating reports
357console.log(securityMonitor.generateSystemReport());
358console.log(powerMonitor.generateSystemReport());
359
360// Checking if recommended actions are of the correct type
361const powerActions = powerMonitor.getRecommendedActions();
362console.log(`Poziom validity awarii zasilania: ${powerActions.severity}`);
363console.log(`Czas do solutions: ${powerActions.timeToResolve} minut`);This elaborate example shows how conditional types can be used to create a complex system that adapts its behavior and data structure depending on the type of object it's working with. This is especially useful when modeling systems that have many variants but share some common functionality.
Limitations of Conditional Types
Although conditional types are a powerful tool, they also have some limitations:
Limited inference capability - TypeScript has limited ability to infer types in complex scenarios, especially when generic types are used together with conditional types.
Type erasure at runtime - like other advanced type features in TypeScript, conditional types exist only at compile time. At runtime, there is no information about conditional types, which can lead to difficulties in implementing certain patterns.
Complexity for beginners - conditional types, especially combined with other advanced type features, can be difficult to understand for TypeScript beginners.
When to Use Conditional Types?
Conditional types are especially useful in the following scenarios:
Creating flexible APIs - when you want your functions or methods to behave differently depending on the type of arguments passed.
Implementing advanced generic tools - when building complex tools that must work with different data types.
Modeling complex business systems - when dealing with different variants of business objects that share common features but differ in details.
Creating type transformation tools - when you need to map one type to another conditionally.
Summary
Conditional types in TypeScript are an advanced but extremely useful tool that allows creating flexible and type-safe APIs. Just as geneticists in Jurassic Park can adapt their protocols to different dinosaur DNA types, conditional types allow us to adapt our code's behavior to different data types while maintaining full support for static typing.
In the following exercises, we will continue exploring advanced features of the TypeScript type system and see how they can help us create safer, more flexible, and easier-to-maintain code.
Code for this lesson: index.ts
1// Conditional types - Jurassic Park
2console.log("=== CONDITIONAL TYPES ===\n");
3
4// ===========================================
5// 1. A basic conditional type
6// ===========================================
7
8type IsCarnivore<T> = T extends { diet: "carnivore" } ? true : false;
9
10interface Carnivore {
11 name: string;
12 diet: "carnivore";
13 biteForce: number;
14}
15
16interface Herbivore {
17 name: string;
18 diet: "herbivore";
19 favoriteFood: string;
20}
21
22// A conditional type is resolved at compile time
23type RexCheck = IsCarnivore<Carnivore>; // true
24type CeraCheck = IsCarnivore<Herbivore>; // false
25
26// ===========================================
27// 2. Extract and Exclude
28// ===========================================
29
30type DinosaurType = "T-Rex" | "Velociraptor" | "Triceratops" | "Brachiosaurus" | "Stegosaurus";
31
32// Extract - picking out the matching types
33type Predator = Extract<DinosaurType, "T-Rex" | "Velociraptor">;
34// Result: "T-Rex" | "Velociraptor"
35
36// Exclude - removing the matching types
37type Gentle = Exclude<DinosaurType, "T-Rex" | "Velociraptor">;
38// Result: "Triceratops" | "Brachiosaurus" | "Stegosaurus"
39
40// ===========================================
41// 3. infer - extracting types from constructs
42// ===========================================
43
44type DinosaurData = {
45 name: string;
46 stats: { weight: number; height: number; speed: number };
47 abilities: string[];
48};
49
50// Extracting a type from a nested structure
51type StatsType = DinosaurData["stats"]; // { weight: number; height: number; speed: number }
52
53// infer in conditional types
54type UnpackArray<T> = T extends Array<infer U> ? U : T;
55type AbilityType = UnpackArray<DinosaurData["abilities"]>; // string
56
57type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
58type ResolvedDino = UnpackPromise<Promise<DinosaurData>>; // DinosaurData
59
60// ===========================================
61// 4. ReturnType and Parameters
62// ===========================================
63
64function createDinosaur(name: string, weight: number, isDangerous: boolean) {
65 return {
66 name,
67 weight,
68 isDangerous,
69 createdAt: new Date()
70 };
71}
72
73type DinoResult = ReturnType<typeof createDinosaur>;
74type DinoParams = Parameters<typeof createDinosaur>;
75
76// ===========================================
77// 5. NonNullable
78// ===========================================
79
80type MaybeDino = string | number | null | undefined;
81type DefinitelyDino = NonNullable<MaybeDino>; // string | number
82
83// ===========================================
84// 6. Practical example - an API response system
85// ===========================================
86
87type ApiResponse<T> = T extends "dinosaur"
88 ? { id: number; name: string; species: string }
89 : T extends "enclosure"
90 ? { id: number; name: string; capacity: number }
91 : T extends "staff"
92 ? { id: number; name: string; role: string }
93 : never;
94
95function getResponse<T extends "dinosaur" | "enclosure" | "staff">(
96 type: T
97): ApiResponse<T> {
98 if (type === "dinosaur") {
99 return { id: 1, name: "Rexy", species: "T-Rex" } as ApiResponse<T>;
100 } else if (type === "enclosure") {
101 return { id: 101, name: "T-Rex Zone", capacity: 2 } as ApiResponse<T>;
102 }
103 return { id: 1001, name: "Alan Grant", role: "Paleontologist" } as ApiResponse<T>;
104}
105
106// ===========================================
107// Test
108// ===========================================
109
110console.log("--- Extract / Exclude ---");
111const predator: Predator = "T-Rex";
112const gentle: Gentle = "Brachiosaurus";
113console.log("Predator:", predator);
114console.log("Gentle:", gentle);
115
116console.log("\n--- ReturnType ---");
117const dino: DinoResult = createDinosaur("Blue", 15, true);
118console.log("Created dinosaur:", JSON.stringify(dino, null, 2));
119
120console.log("\n--- Conditional API Response ---");
121const dinoResp = getResponse("dinosaur");
122console.log("Dinosaur:", JSON.stringify(dinoResp));
123
124const encResp = getResponse("enclosure");
125console.log("Enclosure:", JSON.stringify(encResp));
126
127const staffResp = getResponse("staff");
128console.log("Staff member:", JSON.stringify(staffResp));
129
130console.log("\n--- NonNullable ---");
131function processDino(value: NonNullable<MaybeDino>): void {
132 console.log("Value:", value, "Type:", typeof value);
133}
134processDino("Rexy");
135processDino(42);
136// processDino(null); // Error!
137
138console.log("\nConditional types = dynamic logic at the type level!");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. What does the conditional type T extends U ? X : Y do in TypeScript?
2. What is the purpose of the infer keyword in conditional types?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create conditional types for dynamic type inference
- Click in order
Arrange the elements of a conditional type: