Type Narrowing in TypeScript

Learn via video courses
Topics Covered

Overview

Dynamic checks and predicates provide the information about values at runtime, So, Type narrowing is the process of providing this information during the compile time.

TypeScript follows some possible paths of execution that the programs can take to analyze the most specific possible type of a value at a given position. It searches for some special checks known as Type Guards and assignments. The process of refining types to some more specific types is known as Narrowing.

Introduction

First of all, we will try to get the intuition of type narrowing from a technical perspective of subtyping. The subtyping relation:

relates two types, T and U, which states that T is a subtype of U. So, we can say that T has more information than U, or T is more precise than U.

Given a value x of type T, where T <: U, we are allowed to use x at type U. For example, for a value of x of literal type true, we can view x as having type boolean as true <: boolean.

Given a value of x of type U where T <: U, we cannot view x at type T as type U is less precise than type T. Hence, we will be required to invoke type information from nothing. Also, Dynamic checks and predicates provide extraction of information from values. The extracted information from dynamic predicates further provides the transition from a less precise type U to a more precise type T.

Type Narrowing

In TypeScript, type narrowing is the removal of types from a union. To narrow a variable to a specific type, we use the type guard. We can use the typeof operator with the variable name and compare it with the type that you expect for the variable. In other words, type narrowing is a method to move a type from a less precise type to a more precise type. As the variable of a union type can have one of several different types, we can conclude the correct variable type using type narrowing.

Type Narrowing by Refinement

In the type narrow by refinement, a solid type is refined to a more precise type. Two actions are performed in refinement: replacement and filtering.

In the above snippet, The initial type of x is union type number | boolean, i.e, x is either number or boolean. In the body of the if statement, we know x must have the type number, so we will filter the type boolean from the union. The replacement action will replace a less precise type for a more precise type if we don't have a union type.

In the above snippet, the initial type of x is the type unkown that can't be directly filtered, instead, we will replace the type with the more precise one when narrowing the type of x. In the body of the if statement, x must have the type string, and significantly we will replace the type of x with the type string.

Filtering and replacement can be composed to narrow a type.

In the above snippet, we use a different predicate to narrow x: equality. To narrow x, we filter the type number from the union, because no number is equal to name, also we can replace the typed string with the literal type name.

Type Assertion by Refinement

In some cases, when we use generics we don't have enough information about the type we want to narrow to apply filtering or replacement.

In the above snippet, the type T represents an unknown type. We know that x is a number, we cannot filter the original type T because it is a type parameter, not a union. Also, we cannot replace the type T with the number as information can be lost.

Rather than refining the type, we can add an assertion to the type that contains the new information we obtained. Assertion takes the form of an intersection type U & T. We assert that x is of type U, or x has the intersection type U & T.

In the above snippet, x is a number and generic type of T, we narrow the intersection type T & number. The narrowing allows us to apply function f to x, and also allows us to add x to another number.

Using TypeOf to perform Type Narrowing

The typeof operator is used to implement a TypeScript type guard to assess the type of variable including number, boolean, or string.

In the above snippet, we have a num function that accepts a string or number. When a string is passed, we will repeat it. If a number is given, then we will multiply it by three. To achieve this, we will use the typeof operator to narrow down our input and handle each case in a way that is understandable by TypeScript. So, if we pass num(10) it will return 30 as the output.

Using In and InstanceOf Operators to Perform Type Narrowing

Using in Operator

If the variable is a union type, TypeScript provides another type guard using the in operator to check the property of the variable. It is used in the format of property in the object where property is the name of the property specified we want to check if the property is present inside the object.

In the above snippet, we have to make a function to get the total length of a movie or series. The above snippet will work as we can check for a top-level field that is unique to Movie using the in operator, and handle another case (show type) separately.

Using InstanceOf Operator

It is used to check if a value is an instance of a specific class. It returns a boolean value. When we check a value if it is the instance of a class, TypeScript will assign that type to the variable, hence narrowing the type.

In the above snippet, the function convertDateToString takes in a date, which can be either a string or date. If the function is a date, then the function will be converted to a string otherwise, it will remain the same. We will make use of instanceOf to check whether the function is an instance of date and convert it to a string.

Conclusion

  • There are two key forms of narrowing: refinement and assertion.
  • In refinement, a solid type is refined to a more precise type. Filtering and Replacement are the two actions performed in refinement.
  • Assertion is done when we don't have enough information about types and it is usually performed in Generics.
  • A typeof guard is a significant way to narrow a union of primitive types.
  • The instanceOf type guard is useful for narrowing class types.
  • The in type guard is a significant way to narrow object types.
  • Hope this article has provided you with the relevant information regarding Type Narrowing and its importance in the TypeScript code.