TypeScript Type Narrowing: Writing Safer Code with Precision
When working with TypeScript, you often deal with union types, meaning a variable can have multiple possible types. But how do you safely determine the actual type at runtime? This is where type narrowing comes in.
TypeScript narrowing means refining a variable’s type within a certain scope based on runtime checks. This allows the TypeScript compiler to provide better type inference and prevent unnecessary type errors.
🔍 Why Type Narrowing Matters
Consider this simple function:
function printLength(value: string | string[]) {
return value.length;
}
Here, value
could be either a string
or an array of strings
. But TypeScript will complain because .length
is valid for both types, yet they behave differently (a string
’s .length
is a character count, while an array’s .length
counts items).
We can fix this with narrowing.
🔹 1. Using typeof
for Primitive Types
The typeof
operator helps us narrow down primitives like string
, number
, and boolean
:
function printLength(value: string | string[]) {
if (typeof value === "string") {
console.log(`String length: ${value.length}`);
} else {
console.log(`Array length: ${value.length}`);
}
}
Now TypeScript knows that inside if (typeof value === "string")
, value
must be a string. Otherwise, it must be an array.
🔹 2. Using instanceof
for Class-Based Types
When dealing with class instances, instanceof
helps TypeScript determine an object’s type:
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
Here, instanceof
ensures TypeScript treats animal
as a Dog
inside the if
block and as a Cat
otherwise.
🔹 3. Creating Custom Type Guards
For more complex cases, TypeScript allows custom type guards. These are functions that return a boolean while asserting the type:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript now knows pet is a Fish
} else {
pet.fly();
}
}
The function isFish
checks whether swim
exists, allowing TypeScript to infer that pet
is a Fish
.
✅ Best Practices for Type Narrowing
- Always prefer
typeof
andinstanceof
when dealing with primitive or class-based types. - Use custom type guards for more complex scenarios.
- Leverage discriminated unions (objects with a common
type
field) for better readability.
🚀 Final Thoughts
Type narrowing is an essential TypeScript skill that makes your code safer and more predictable. Instead of making risky type assertions (as any
), you can let TypeScript do the work for you by refining types intelligently.
Next time you work with union types, think about narrowing—your future self (and your TypeScript compiler) will thank you! operator helps us narrow down primitives like string, number, and boolean: