DEV Community

Cover image for Type Narrowing in TypeScript
Maina Wycliffe for This is Learning

Posted on • Originally published at mainawycliffe.dev

Type Narrowing in TypeScript

In the spirit of my last few articles, where we have looked into Template Literal Types and Types and Mocking, we are going to dissect another topic in typescript involving types. In this article, we are going to learn various ways you can narrow types. Type narrowing is the process of moving a type from a less precise type to a more precise type.

Let's start with a simple function:

function friends(input: string | number) {
    // code here
}
Enter fullscreen mode Exit fullscreen mode

The above function can either take a number or a string. Let's say we want to perform different actions based upon whether input is a number or a string. In this case, we will use Javascripts type guards to check if it's a string or number, as shown below:

function someFunc(input: string | number) {
  if(typeof input === "string") {
    // do something with the string
    console.log("input is a string");
  }

  if(typeof input === "number") {
    // do something with number
    console.log("input is a number");
  }
}
Enter fullscreen mode Exit fullscreen mode

Type Guards

In the above example, we used Javascripts type guards to narrow the type of input to either number or string. Type guards are used to check if a variable is of a certain type, i.e. number, string, object, etc. When a type guard is used, Typescript expects that variable to be of that type. It will automatically type check its usage based on that information.

Here is a list of Javascripts type guards available:

string

if(typeof param === "string") {
  // do something with string value
}
Enter fullscreen mode Exit fullscreen mode

number

if(typeof param === "number") {
  // do something with number value
}
Enter fullscreen mode Exit fullscreen mode

bigint

if(typeof param === "bigint") {
  // do something with bigint value
}
Enter fullscreen mode Exit fullscreen mode

boolean

if(typeof param === "boolean") {
  // do something with boolean value
}
Enter fullscreen mode Exit fullscreen mode

symbol

if(typeof param === "symbol") {
  // do something with symbol value
}
Enter fullscreen mode Exit fullscreen mode

undefined

if(typeof param === "undefined") {
  // do something with undefined value
}
Enter fullscreen mode Exit fullscreen mode

object

if(typeof param === "object") {
  // do something with object value
}
Enter fullscreen mode Exit fullscreen mode

function

if(typeof param === "function") {
  // do something with the function
}
Enter fullscreen mode Exit fullscreen mode

Truthiness Narrowing

In this type of narrowing, we check whether a variable is truthy before using it. When a variable is truthy, typescript will automatically remove the possibility of that variable being falsy i.e. undefined or null, etc, within the conditional check.

Take for instance the following example, where a function someFunction below takes an input, whose type is either a string or undefined (i.e. optional).

function someFunction(x?: string) {
  if(x) {
    console.log(typeof x) // "string"
  }
}
Enter fullscreen mode Exit fullscreen mode

By checking whether input **is truthy, the type of **x becomes a string otherwise it's undefined.

Equality Narrowing

If two variables are equal, then the types of both variables must be the same. If one variable is of an imprecise type (i.e. unknown, any etc.) and is equal to another variable of a precise type, then typescript will use that information to narrow the type of the first variable.

Take the following function, which takes two parameters: x and y, with x being either a string or a number and y being a number. When the value of x is equal to the value of y, then the type of x is inferred to be a number and otherwise a string.

function someFunction(x: string | number, y: number) {
    if(x === y) {
        // narrowed to number
        console.log(typeof x) // number
    } else {
        // this is not narrowed
        console.log(typeof x) // number or string
    }
}
Enter fullscreen mode Exit fullscreen mode

Discriminated Unions

In this approach, you create an object, with a literal member that can be used to discriminate between two different unions. Let's take an example of a function that calculates the square of different shapes - Rectangle and Circle. We will start by defining the type of Rectangle and Circle.

type Rectangle = {
    shape: "reactangle",
    width: number;
    height: number;
}

type Circle = {
    shape: "circle"
    radius: number;
}
Enter fullscreen mode Exit fullscreen mode

From the above types, the objects will each have the literal field of shape, which can either be a circle or rectangle. We can use the shape field within our function to calculate area, that would accept a union of Rectangle and Circle, as shown below:

function calculateArea(shape: Rectangle | Circle) {
    if(shape.shape === "reactangle") {
        // you can only access the properties of reactangle and not circle
        console.log("Area of reactangle: " + shape.height * shape.width);
    }

    if(shape.shape === "circle") {
        // you can only access the properties of circle and not reactangle
        console.log("Area of circle: " + 3.14 * shape.radius * shape.radius);
    }
}
Enter fullscreen mode Exit fullscreen mode

When the shape field is a rectangle, you only have access to properties available in the Rectangle type, that is width, height and shape. The same applies to when shape field is a circle, typescript will only allow you to access radius and circle and will throw an error otherwise.

Using the in Operator for Narrowing

The in operator is used to determine if an object has a property with a name in it. It's used in the format of "property" in object where property is the name of the property you want to check if it exists inside the object.

In the example above, we used discriminated unions to distinguish between a Circle and Rectangle. We can also use the in operator to achieve the same, but this time we will be checking if a shape contains certain properties i.e. radius for Circle, width and height for Rectangle, and the results would be the same.

type Circle = {
  radius: number;
};

type Reactangle = {
  width: number;
  height: number;
};

function calculateArea(shape: Circle | Reactangle) {
  if ("radius" in shape) {
    // now you can access radius from shape
    console.log("Area of circle: " + 3.14 * shape.radius * shape.radius);

    // any attempt to access height or width will result to an error
    shape.width; // Property 'width' does not exist on type 'Circle'.
    shape.height; // Error: Property 'height' does not exist on type 'Circle'
  }
  if ("width" in shape && "height" in shape) {
    // now you can access height and width from the shape object
    console.log("Area of reactangle: " + shape.height * shape.width);

    // any attempt to access raidus would result to an error
    shape.radius; // Error: Property 'radius' does not exist on type 'Reactangle'.ts
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Assignment Narrowing

In this type of narrowing, typescript will narrow the type of a variable once it's assigned a value. Take a variable x of union type of either number or string, if we assign it a number, the type becomes a number and if we assign it a string, the type changes to a string instead.

let x : number | string = 1;

console.log(typeof x) // "number"

x = "something"

console.log(typeof x) // "string"
Enter fullscreen mode Exit fullscreen mode

Here is a detailed example at Code Sandbox:

Using instanceof for Narrowing

Javascripts' instanceof operator is used to check if a value is an instance of a certain class. It's used in the format of value instanceof value2 and returns a boolean. When you check if a value is an instanceof a class, Typescript will assign that type to the variable, thereby narrowing the type.

Take the following example, where a function takes in a date, which can be either a string or a Date. If it's a Date, we want to convert it to a string and if it's a string, we will return it as is. We can use instanceof to check if it's an instance of a Date and convert it to string, as shown below.

function dateToString(value: string | Date) {
  if(value instanceof Date) {
    // The type now is Date and you can access Date methods
    return value.toISOString();
  }
  return value;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we learned various ways we can narrow types, from type guards to discriminated unions. In our next article, we will learn how we can build our own type guards using type predicates.

If you found this article informative and would like to keep learning, visit my new series on Typescript - A Byte of Typescript. A Byte of Typescript is a new series that I will be publishing on a regular basis to help you demystify Typescript.

Discuss this Article

Top comments (4)

Collapse
 
mainawycliffe profile image
Maina Wycliffe

Thanks. To be honest, no reason, just a forceful habit. Over the years, the gap between interfaces and types aliases keeps closing and as of the moment, they can be mostly interchanged without major issue. I use type aliases mostly for consistency and it became a habit.

Collapse
 
pobx profile image
Pobx • Edited

Thank you. It's amazing article. I love your short explanation foreach section. It's make me clear about it. I have question about "Discriminated Unions" and "in operator". Which one I should use for narrow type. now it's make me a little bit confuse because it's may be same thing.

Collapse
 
mainawycliffe profile image
Maina Wycliffe

I would lean with discriminated unions over using the in operator because they are easier to use. With the in operator, you might find yourself writing more complex checks if two or more types have a lot of overlapping properties. For simple check though, you can use them interchangeably, with no issue at all.