TypeScript provides a powerful feature called "type narrowing" that allows you to make your code more precise by narrowing down the type of a variable within a certain block of code.
This feature helps you write safer and more maintainable code by leveraging TypeScript's static type checking.
In this article, we'll explore the basics of type narrowing and gradually delve into more advanced scenarios.
Basic type narrowing
typeof
Narrowing
function printMessage(message: string | number) {
if (typeof message === 'string') {
// Within this block, TypeScript knows that `message`
// is a string
console.log(message.toUpperCase());
} else {
// Here, TypeScript knows that `message` is a number
console.log(message.toFixed(2));
}
}
printMessage('Hello'); // Output: HELLO
printMessage(42); // Output: 42.00
User-Defined Type Guards aka ( T : is K )
interface Cat {
type: 'cat';
meow(): void;
}
interface Dog {
type: 'dog';
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return animal.type === 'cat';
}
function handleAnimal(animal: Cat | Dog) {
if (isCat(animal)) {
// TypeScript now knows that `animal` is a Cat
animal.meow();
} else {
// TypeScript knows that `animal` is a Dog
animal.bark();
}
}
Discriminated Unions Aka additional prop:value
This sounds a bit sophisticated but it's very simple.
type Circle = { kind: 'circle', radius: number }
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Shape = Circle | Rectangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript knows `shape` is a circle here
return Math.PI * Math.pow(shape.radius, 2);
case 'rectangle':
// TypeScript knows `shape` is a rectangle here
return shape.width * shape.height;
}
}
Here by adding the attribute kind
we are telling typescript
what type we are expecting.
don't get intimidated by this one, in real life apps this can be one of these like
__type__: something
or __typename
if you are familiar with graphql
instance of
type guard
class Car {
drive() {
console.log('Vroom!');
}
}
class Bike {
ride() {
console.log('Ring ring!');
}
}
function handleVehicle(vehicle: Car | Bike) {
if (vehicle instanceof Car) {
// TypeScript knows now `vehicle` is an instance of Car
vehicle.drive();
} else {
// TypeScript knows `vehicle` is an instance of Bike
vehicle.ride();
}
}
is
type guard
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFishOrBird(pet: Fish | Bird): pet is Fish {
return 'swim' in pet;
}
function handlePet(pet: Fish | Bird) {
if (isFishOrBird(pet)) {
// TypeScript knows `pet` is a Fish
pet.swim();
} else {
// TypeScript knows `pet` is a Bird
pet.fly();
}
}
Bonus the extends
utility
In contrast of type narrowing and how we can use it to tell
typescript how to understand our types.
There are instances we want to use generics
// Base type with a timestamp property
type TimestampedObject = {
timestamp: Date;
};
// Generic function that works with timestamped objects
// by telling typescript T needs to be an object that has a
// timestamp property where timestamp is a Date type
function processTimestampedObject<T extends TimestampedObject>(obj: T): void {
// Access the timestamp property safely
const timestamp: Date = obj.timestamp;
// Perform some processing with the timestamp
console.log(`Processing timestamp: ${timestamp.toISOString()}`);
}
Now if you try to run the previous code
// This works fine because the timestamp property is a Date
const validObject: TimestampedObject = { timestamp: new Date() };
processTimestampedObject(validObject);
// This would cause a TypeScript error without using `extends`
const invalidObject: TimestampedObject = { timestamp: '2022-01-01' };
processTimestampedObject(invalidObject);
Conclusion
Incorporating these techniques into your codebase will lead to safer and more predictable TypeScript applications.
Also checkout
10 typescript utilities you need to know
If you find this useful please hit the 👍 button
Cheers
Top comments (1)
This article has way to few likes! It's actually a really good explanation of type narrowing 👌