DEV Community

Cover image for TypeScript Type Narrowing: A Comprehensive Guide 🤖
hichem ben chaabene
hichem ben chaabene

Posted on

TypeScript Type Narrowing: A Comprehensive Guide 🤖

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
Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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()}`);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
brense profile image
Rense Bakker

This article has way to few likes! It's actually a really good explanation of type narrowing 👌