This document covers Type Narrowing and User-Defined Type Guards and explains how these features make TypeScript's type system more flexible and precise.
Background
Union Type
A union type in TypeScript combines two or more types into one, indicating that a value can be of any of the specified types. When using a union type, you can only access members that are common to all the possible types of the variable or parameter.
In other words, while union types allow you to handle various types with a single variable, it requires caution to maintain type safety. Let's look at an example:
interface Cat {
name: string;
meow: () => void;
}
interface Dog {
name: string;
bark: () => void;
}
function getAnimal(): Cat | Dog {
// Returns either a 'Cat' or a 'Dog'
}
let animal = getAnimal();
animal.name(); // This is safe because both 'Cat' and 'Dog' have 'name' method
// animal.meow(); // Error: Property 'meow' does not exist on type 'Dog'
In the code above, the getAnimal
function returns a Cat
or Dog
type. Therefore, the animal
variable has a Cat | Dog
type, and you can call the name
method, which is common to both types, but not the meow
method, which only exists on the Cat
type.
💬 So, how can you safely use methods that only exist on a specific type? This is the reason for writing this document.
When using union types in TypeScript, to safely access properties or methods unique to a specific type, you need to use type narrowing techniques. Let's explore how type narrowing and user-defined type guards can be effectively utilized.
Type Narrowing
Type narrowing is the process of reducing the range of possible types for a variable in TypeScript. TypeScript tries to make type analysis as precise as possible to enhance type safety.
typeof
operator: Used to narrow down primitive types (e.g.,string
,number
,boolean
).instanceof
operator: Used to check if an object is an instance of a class and narrow the type accordingly.in
operator: Used to check if an object has a certain property and narrow the type based on that.- User-defined type guards: Custom functions that narrow types based on specific conditions.
User-Defined Type Guards
Sometimes, we want more control over how types change in our code. This is where user-defined type guards come in. A user-defined type guard is a function that you define to narrow down a type based on certain conditions.
Defining a User-Defined Type Guard
To define a user-defined type guard, you create a function with a return type that is a type predicate.
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
- In the function above,
animal is Cat
is the type predicate. A type predicate has the formparameterName is Type
, whereparameterName
must be the name of a parameter in the current function. - Type predicate (
animal is Cat
): This predicate tells TypeScript that if the function returnstrue
, theanimal
is of typeCat
.
Using Type Guards to Narrow Types
TypeScript infers the type of a variable based on the result of the function call.
let animal = getAnimal();
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
- In this code, TypeScript recognizes that if
isCat(animal)
returnstrue
, thenanimal
is of typeCat
and allows themeow
method to be called. Conversely, if it returnsfalse
,animal
is recognized as aDog
and thebark
method can be called.
Using Type Guards with Arrays
You can also use type guards to filter specific types from an array. Here’s how to extract only Cat
types from an array of Cat | Dog
elements.
const animals: (Cat | Dog)[] = [getAnimal(), getAnimal(), getAnimal()];
const cats: Cat[] = animals.filter(isCat);
In this example, the isCat
type guard is used to filter out only Cat
type elements from the animals
array. The result is a new array of type Cat[]
.
Summary
- Type Narrowing is the process of restricting a variable to a more specific type.
- User-Defined Type Guards are custom functions that use type predicates to narrow types based on conditions.