Type Narrowing and Type Guards in TypeScript
Type Narrowing and Type Guards
~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Time to fulfill the promise from chapter 9: HOW do you access type-specific properties inside a union type? The answer is type narrowing – TypeScript automatically NARROWS a variable's type based on runtime checks.
typeof: for primitive types
function formatId(id: string | number): string {
if (typeof id === 'string') {
return id.toUpperCase(); // TypeScript knows HERE: id is string
}
return id.toFixed(2); // TypeScript knows HERE: id is number (process of elimination)
}typeof id === 'string' is a RUNTIME check (real JavaScript, exists in the compiled code) – TypeScript RECOGNIZES this pattern automatically and narrows id to string INSIDE the if block, and to number in the else branch (implicitly, via the return).
instanceof: for class instances
function describeMedium(medium: Book | Audiobook): string {
if (medium instanceof Audiobook) {
return `${medium.title}, narrated by ${medium.narrator}`; // narrator only on Audiobook!
}
return `${medium.title}, ${medium.pageCount} pages`; // pageCount only on Book!
}instanceof checks whether an object is actually an instance of a SPECIFIC class – only works with classes (not with pure interfaces/type aliases, since those don't exist at runtime, see chapter 1).
The in operator: checking property existence
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
function move(animal: Bird | Fish): void {
if ('fly' in animal) {
animal.fly(); // TypeScript knows: animal is Bird
} else {
animal.swim(); // TypeScript knows: animal is Fish
}
}in is especially useful for PURE interfaces (no classes, so no instanceof option) – checks at runtime whether a property/method EXISTS on the object.
Discriminated unions again: the most elegant approach
Chapter 9 already showed the type-field pattern – now we understand WHY it works: if (match.type === 'book') is structurally IDENTICAL to typeof narrowing, just applied to a SELF-DEFINED literal field instead of a built-in JavaScript type.
Writing custom type guards
function isAudiobook(medium: Medium): medium is Audiobook {
return medium instanceof Audiobook;
}
const media: Medium[] = [/* ... */];
const audiobooks = media.filter(isAudiobook);
// audiobooks has the type Audiobook[], NOT Medium[] -
// TypeScript understands the 'medium is Audiobook' signature as a narrowing instructionmedium is Audiobook (instead of simply boolean) is a "type predicate" signature – it tells TypeScript: "if this function returns true, NARROW the parameter's type to Audiobook". This ALSO works with Array.filter(), as shown above – an extremely useful, advanced pattern.
Exhaustiveness checking with never
type Genre = 'Fantasy' | 'Mystery' | 'NonFiction';
function genreIcon(genre: Genre): string {
switch (genre) {
case 'Fantasy': return '????';
case 'Mystery': return '????';
case 'NonFiction': return '????';
default:
const neverReached: never = genre; // error, IF Genre gets extended with a value
// but this switch wasn't updated!
throw new Error(`Unknown genre: ${neverReached}`);
}
}never from chapter 4 in its most useful application: once ALL cases of the union have been handled, genre correctly has the type never in the default branch ("logically cannot reach here"). If Genre gets extended LATER with 'Novel', but THIS switch is forgotten, TypeScript IMMEDIATELY shows an error at exactly this spot – a powerful safety net against "forgotten" case distinctions.
Tipp: Rule of thumb: typeof for primitive types, instanceof for classes, in for pure interface properties, a discriminator field for self-defined unions (usually the MOST elegant approach), custom type guards (x is Y) for more complex, reusable check logic. The never technique is optional but very valuable as unions grow.