Introduction to Type Narrowing in TypeScript

Introduction to Type Narrowing in TypeScript

TypeScript is a powerful language that helps us add a layer of security to our code. It makes refactoring easy with its static type checking system. Static type checkers identify the problems before running the code and decreasing errors significantly during the build time. With the power of inference, TypeScript makes development really easy. Let us see how we can use Type Narrowing in TypeScript and what it is all about.

Type Guards

Type Guards are expressions that perform a runtime check and help to narrow down on the type.

function transform(input: string | number) {
  return input.toUpperCase(); // will throw an error: Property 'toUpperCase' does not exist on type 'string | number'.
}
transform("hi");

We see this error as input can be both a string and number and we can perform toUpperCase() only on a string. We need to first check the type of input here to perform any operations on it. This can be accomplished using the typeof operator which is a type guard in TypeScript.

function transform(input: string | number) {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  return input + 4;
}

transform("hi");

Now, TypeScript can infer that if the input is a string type, we can call all the methods and properties that are allowed on strings. This is known as type narrowing.

Equality Narrowing

function check(a: number | string, b: number | boolean) {
  if (a === b) {
    return a + b;
  }
}
check(3, 3);

Here, TypeScript checks the type of both a and b and as the common type is that of a number, it performs add operation on a and b after equality narrowing.

The in Operator

The in operator checks if the property exists on the object for type narrowing.

type Appetizer = {
  eat: () => void;  
}

type Beverage = {
  drink: () => void;
}

function eat(food: Appetizer | Beverage) {
  if ('eat' in food) {
    return food.eat();
  }

  if ('drink' in food) {
    return food.drink();
  }
}

In the example, we check if the 'eat' or 'drink' property exists on food. As TypeScript recognizes 'in' as a type guard, we can narrow the type using it.

Control Flow Analysis

For the above example, we can modify our code as follows:

type Appetizer = {
  eat: () => void;
};

type Beverage = {
  drink: () => void;
};

function eat(food: Appetizer | Beverage) {
  if ("eat" in food) {
    return food.eat();
  }
  return food.drink();
}

We can get rid of the second 'if statement'. Here, TypeScript automatically infers that we have only two types and during the second return, you will notice that TS gives 'drink' as a suggestion on the 'food' object. This power and intelligence of TS makes writing code really easy and enhances the Developer experience!

Discriminated Unions

Discriminated unions help narrow down the members of a union. In the following example, TS will throw an error whenever we define any third shape as only "rhombus" and "square" are accepted shapes.

type Polygon = {
  shape: "rhombus" | "square";
}

 function handleShape(input: Polygon) {
   if (input.shape === "circle") {
// This will always return 'false' since the types '"rhombus" | "sqaure"' and '"circle"' have no overlap.
  }
}

Conclusion

Type driven development makes the code more robust and readable. You can see how with the help of type narrowing, writing code becomes easy and intuitive. I hope this blog will help clear your basics of type narrowing.

To check my work, you can connect with me on Twitter. Happy Coding!