"In Jurassic Park, it's not enough to know you have a dinosaur in front of you," says Robert Muldoon, the park's chief game warden. "You need to know exactly WHETHER it's a predator, WHETHER it's a herbivore, WHETHER it flies, WHETHER it swims. The security protocol you activate depends on that. One wrong assumption and you have a catastrophe."
Type narrowing in TypeScript works similarly - it's the process of narrowing a broad type to a more specific one. When a variable has type
string | number, TypeScript doesn't know whether it's text or a number. With type guards, we can "narrow" the type so the compiler knows exactly what it's dealing with. This is a fundamental technique for writing safe TypeScript code.The simplest type guard is
typeof - a JavaScript operator that TypeScript understands and uses for type narrowing. When you check the type of a variable using typeof, the compiler automatically knows what type the variable has inside the if block.1// Function accepting different types of dinosaur data
2function formatDinosaurData(value: string | number | boolean): string {
3 if (typeof value === "string") {
4 // TypeScript knows that value is of type string here
5 return value.toUpperCase();
6 }
7 if (typeof value === "number") {
8 // TypeScript knows that value is of type number here
9 return value.toFixed(2) + " kg";
10 }
11 // TypeScript knows that value is of type boolean here
12 return value ? "Active" : "Inactive";
13}
14
15console.log(formatDinosaurData("Tyrannosaurus Rex")); // "TYRANNOSAURUS REX"
16console.log(formatDinosaurData(7500)); // "7500.00 kg"
17console.log(formatDinosaurData(true)); // "Active"In the above example, TypeScript automatically narrows the type of
value in each if block. After checking typeof value === "string", the compiler knows that inside that block value is a string, so you can safely call .toUpperCase(). This works with types "string", "number", "boolean", "bigint", "symbol", "undefined", "object", and "function".When working with classes,
instanceof allows you to check whether an object is an instance of a specific class. TypeScript automatically narrows the type after such a check.1class Carnivore {
2 name: string;
3 biteForce: number;
4
5 constructor(name: string, biteForce: number) {
6 this.name = name;
7 this.biteForce = biteForce;
8 }
9
10 hunt(): string {
11 return `${this.name} hunts with bite force of ${this.biteForce}N!`;
12 }
13}
14
15class Herbivore {
16 name: string;
17 favoriteFood: string;
18
19 constructor(name: string, favoriteFood: string) {
20 this.name = name;
21 this.favoriteFood = favoriteFood;
22 }
23
24 graze(): string {
25 return `${this.name} spokojnie zjada ${this.favoriteFood}`;
26 }
27}
28
29// Function handling both dinosaur types
30function handleDinosaur(dino: Carnivore | Herbivore): string {
31 if (dino instanceof Carnivore) {
32 // TypeScript knows that dino is Carnivore here
33 return dino.hunt(); // We have access to hunt() and biteForce
34 }
35 // TypeScript knows that dino is Herbivore here
36 return dino.graze(); // We have access to graze() and favoriteFood
37}
38
39const rex = new Carnivore("T-Rex", 12800);
40const trike = new Herbivore("Triceratops", "Paprocie");
41
42console.log(handleDinosaur(rex)); // "T-Rex hunts with bite force of 12800N!"
43console.log(handleDinosaur(trike)); // "Triceratops spokojnie zjada Paprocie"The
in operator checks whether an object has a given property. TypeScript uses this to narrow types in union types.1interface FlyingDinosaur {
2 name: string;
3 maxAltitude: number;
4 wingspan: number;
5}
6
7interface SwimmingDinosaur {
8 name: string;
9 maxDepth: number;
10 swimSpeed: number;
11}
12
13interface LandDinosaur {
14 name: string;
15 runSpeed: number;
16 weight: number;
17}
18
19type Dinosaur = FlyingDinosaur | SwimmingDinosaur | LandDinosaur;
20
21function describeDinosaur(dino: Dinosaur): string {
22 if ("maxAltitude" in dino) {
23 // TypeScript knows this is FlyingDinosaur
24 return `${dino.name} flies up to altitude of ${dino.maxAltitude}m, wingspan: ${dino.wingspan}m`;
25 }
26 if ("maxDepth" in dino) {
27 // TypeScript knows this is SwimmingDinosaur
28 return `${dino.name} dives to ${dino.maxDepth}m, speed: ${dino.swimSpeed} km/h`;
29 }
30 // TypeScript knows this is LandDinosaur
31 return `${dino.name} runs at speed of ${dino.runSpeed} km/h, weighs ${dino.weight} kg`;
32}
33
34const pteranodon: FlyingDinosaur = { name: "Pteranodon", maxAltitude: 500, wingspan: 7 };
35const mosasaurus: SwimmingDinosaur = { name: "Mosasaurus", maxDepth: 200, swimSpeed: 48 };
36const velociraptor: LandDinosaur = { name: "Velociraptor", runSpeed: 64, weight: 15 };
37
38console.log(describeDinosaur(pteranodon));
39console.log(describeDinosaur(mosasaurus));
40console.log(describeDinosaur(velociraptor));The
in operator is particularly useful when working with interfaces (not classes), because instanceof only works with classes, while in simply checks for the presence of a property.You can create your own type guard functions that return the special type
parameterName is Type. This way TypeScript understands that after a positive check, the variable has a specific type.1interface DinoEgg {
2 species: string;
3 weight: number;
4 incubationDays: number;
5}
6
7interface HatchedDino {
8 species: string;
9 weight: number;
10 age: number;
11 enclosureId: string;
12}
13
14// Custom type guard - function returns "specimen is HatchedDino"
15function isHatched(specimen: DinoEgg | HatchedDino): specimen is HatchedDino {
16 return "age" in specimen && "enclosureId" in specimen;
17}
18
19function processSpecimen(specimen: DinoEgg | HatchedDino): string {
20 if (isHatched(specimen)) {
21 // TypeScript knows that specimen is HatchedDino here
22 return `${specimen.species} - wiek: ${specimen.age} dni, zagroda: ${specimen.enclosureId}`;
23 }
24 // TypeScript knows that specimen is DinoEgg here
25 return `Jajo ${specimen.species} - inkubacja: ${specimen.incubationDays} dni`;
26}
27
28// More advanced type guard with validation
29function isCarnivoreData(data: unknown): data is { species: string; biteForce: number; diet: "carnivore" } {
30 return (
31 typeof data === "object" &&
32 data !== null &&
33 "species" in data &&
34 "biteForce" in data &&
35 "diet" in data &&
36 (data as any).diet === "carnivore"
37 );
38}
39
40// Usage with an unknown data source (e.g., API)
41function processParkData(rawData: unknown): string {
42 if (isCarnivoreData(rawData)) {
43 // TypeScript wie, that rawData ma pola species, biteForce, diet
44 return `Predator: ${rawData.species}, force: ${rawData.biteForce}N`;
45 }
46 return "Nieznany typ danych";
47}
48
49console.log(processParkData({ species: "T-Rex", biteForce: 12800, diet: "carnivore" }));
50console.log(processParkData({ name: "Triceratops" }));Custom type guards are particularly useful when:
unknownDiscriminated unions is a pattern where each type in the union has a common "discriminating" field (tag) that uniquely identifies the type. TypeScript excels at narrowing types based on this field.
1// Each type has a "kind" field as the discriminator
2interface SecurityAlert {
3 kind: "security";
4 zone: string;
5 threatLevel: number;
6 dinosaurId: string;
7}
8
9interface MedicalAlert {
10 kind: "medical";
11 dinosaurId: string;
12 symptoms: string[];
13 veterinarianRequired: boolean;
14}
15
16interface SystemAlert {
17 kind: "system";
18 component: string;
19 errorCode: number;
20 message: string;
21}
22
23type ParkAlert = SecurityAlert | MedicalAlert | SystemAlert;
24
25function handleAlert(alert: ParkAlert): string {
26 switch (alert.kind) {
27 case "security":
28 // TypeScript knows this is SecurityAlert
29 return `SECURITY ALARM! Zone: ${alert.zone}, Threat: ${alert.threatLevel}/10, Dinosaur: ${alert.dinosaurId}`;
30 case "medical":
31 // TypeScript knows this is MedicalAlert
32 return `ALERT MEDYCZNY! Dinosaur: ${alert.dinosaurId}, Objawy: ${alert.symptoms.join(", ")}`;
33 case "system":
34 // TypeScript knows this is SystemAlert
35 return `AWARIA SYSTEMU! Komponent: ${alert.component}, Kod: ${alert.errorCode}`;
36 }
37}The
never type in TypeScript means "a value that will never occur". We can use it to check whether we've handled ALL variants of a discriminated union. If we add a new variant and forget to handle it, TypeScript will throw a compilation error.1// Exhaustive check - TypeScript will warn us if we miss any variant
2function handleAlertExhaustive(alert: ParkAlert): string {
3 switch (alert.kind) {
4 case "security":
5 return `Security: ${alert.zone}`;
6 case "medical":
7 return `Medyczny: ${alert.dinosaurId}`;
8 case "system":
9 return `System: ${alert.component}`;
10 default:
11 // If we add a new alert type and forget to handle it,
12 // TypeScript will report an error, because alert won't be of type never
13 const exhaustiveCheck: never = alert;
14 return exhaustiveCheck;
15 }
16}
17
18// Practical helper for exhaustive check
19function assertNever(value: never, message: string = "Unhandled variant"): never {
20 throw new Error(`${message}: ${JSON.stringify(value)}`);
21}Exhaustive check is a powerful technique - when you add e.g.
WeatherAlert to the ParkAlert union, the compiler will IMMEDIATELY point out all places in the code where handling of the new variant is missing. It's like an early warning system in Jurassic Park - it doesn't let you overlook any threat.Below is a more elaborate example combining different type narrowing techniques in one system:
1// Laboratory sample classification system
2type SampleStatus = "collected" | "analyzing" | "completed" | "contaminated";
3
4interface BaseSample {
5 id: string;
6 collectedAt: Date;
7 status: SampleStatus;
8}
9
10interface DNASample extends BaseSample {
11 type: "dna";
12 species: string;
13 purity: number;
14 sequenceLength: number;
15}
16
17interface BloodSample extends BaseSample {
18 type: "blood";
19 species: string;
20 volumeMl: number;
21 pH: number;
22}
23
24interface FossilSample extends BaseSample {
25 type: "fossil";
26 era: string;
27 estimatedAge: number;
28}
29
30type LabSample = DNASample | BloodSample | FossilSample;
31
32// Custom type guard
33function isDNASample(sample: LabSample): sample is DNASample {
34 return sample.type === "dna";
35}
36
37// Checking status using typeof and custom guard
38function canProcess(sample: LabSample): boolean {
39 return sample.status === "collected" || sample.status === "analyzing";
40}
41
42// Main processing function - combines different techniques
43function processLabSample(sample: LabSample): string {
44 // Discriminated union narrowing
45 switch (sample.type) {
46 case "dna":
47 if (sample.purity < 50) {
48 return `DNA Sample ${sample.id}: purity too low (${sample.purity}%)`;
49 }
50 return `DNA Analysis ${sample.species}: ${sample.sequenceLength} base pairs, purity ${sample.purity}%`;
51
52 case "blood":
53 const phStatus = sample.pH >= 7.0 && sample.pH <= 7.6 ? "normal" : "OUT OF RANGE";
54 return `Analiza krwi ${sample.species}: ${sample.volumeMl}ml, pH ${sample.pH} (${phStatus})`;
55
56 case "fossil":
57 return `Fossil from era ${sample.era}: estimated age ${sample.estimatedAge} million years`;
58
59 default:
60 const _exhaustive: never = sample;
61 return _exhaustive;
62 }
63}
64
65// Test
66const dnaSample: DNASample = {
67 id: "DNA-042", type: "dna", collectedAt: new Date(),
68 status: "collected", species: "Velociraptor", purity: 87, sequenceLength: 75000000
69};
70
71const bloodSample: BloodSample = {
72 id: "BLD-015", type: "blood", collectedAt: new Date(),
73 status: "analyzing", species: "Triceratops", volumeMl: 120, pH: 7.2
74};
75
76const fossilSample: FossilSample = {
77 id: "FOS-003", type: "fossil", collectedAt: new Date(),
78 status: "completed", era: "Kreda", estimatedAge: 68
79};
80
81console.log(processLabSample(dnaSample));
82console.log(processLabSample(bloodSample));
83console.log(processLabSample(fossilSample));Type narrowing and type guards are fundamental TypeScript tools that allow you to write safe code while maintaining the flexibility of union types:
string, number, boolean)As Robert Muldoon says: "Species identification is the first step of the security protocol. Similarly, type narrowing is the first step to safe code - never assume what you're dealing with. Check, narrow the type, and act with confidence."