Beginners Guide for Type Narrowing in TypeScript

Beginners Guide for Type Narrowing in TypeScript

Before moving towards Type Narrowing lets revise some basic things about TypeScript. It provides an additional layer of security to the code. So whenever you refactor your code in future and it will alert you about types of all variables which should be accepted, used and returned by the functions in your code. The type driven development makes refactoring the code easy and error free with its static type checking system. Static type checkers identify problems before running the code and decreasing errors significantly during the build time.

Let us see what is Type Narrowing in TypeScript and how we can use it.

Prerequisites -

As you are here to know about Type Narrowing, I assume that you have -

  • Basic knowledge of JavaScript and TypeScript.
  • Good understanding of primitive, union & class types in TypeScript.

Intoduction -

Lets consider following example -

// example 1
type Shape = {
  name : string;
  sides?: number;
}

function getDoubleSides(shape : Shape){
  return shape.sides * 2;  // sides can be possibly undefined
}

In above code snippet if sides will be undefined then program will not behave correctly. So we can use type narrowing to avoid type errors like the one above.

Type Narrowing -

A variable can move from a less precise type to a more precise type in a typescript program. This is known as type narrowing.

Type Guards -

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

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

We get the 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.

//example 2 solution
function inputTransform(input: string | number) {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  if (typeof input === "number"){
    return input*2;
  }
}

inputTransform("hello"); // will return HELLO
inputTransform(4); // will return 8

Different Ways for Type Narrowing -

1) Using a conditional value check -

Lets consider 'example 1', There TypeScript raises a type error because of the sides property in the 'getDoubleSides' function as it could be undefined, and it doesn’t make sense to multiply 'undefined' with 2. A solution is to check whether sides is truthy before it is doubled.

// example 1 repeated
type Shape = {
  name : string;
  sides?: number;
}

function getDoubleSides(shape : Shape){
  if(shape.sides){
    return shape.sides * 2; 
  }
  return 0;
}

2) Using a typeof type guard -

The typeof type guard is useful for narrowing union types of primitive types. We already saw this in 'example 2' where we are checking the type of input here to perform any operations on it.

//example 2 repeated
function inputTransform(input: string | number) {
  if (typeof input === "string") {
    return input.toUpperCase();
  }
  if (typeof input === "number"){
    return input*2;
  }
}

inputTransform("hello"); // will return HELLO
inputTransform(4); // will return 8

3) Using an instanceof type guard -

The instanceof type guard is useful for narrowing union type of class types. We can determine the object assigned to variable is instance of which class. Lets consider following example -

//example 3
class Teacher {
  constructor(
    public name : string;
    public teacherId: number;
  ) {}
}
class Student {
  constructor(
    public name: string;
    public rollno: number;
  ) {}
}
Type Participant = Teacher | Student;

function getIdOrRollno(participant: Participant) {
  if (participant instanceof Teacher) {
    return particiapant.teacherId;  //if particiapant is teacher then return teacherId
  }
  return participant.rollno;
}

In above example we are checking the class type of participant and then performing suitable operation.

4) Using an in type guard -

The in type guard is useful for narrowing union type of object types. We can determine the key value we want to use is present in type or not. Lets consider following example -

//example 3 with in type guard
interface Teacher = {
  name : string;
  teacherId: number;
}
interface Student = {
  name: string;
  rollno: number;
}
Type Participant = Teacher | Student;

function getIdOrRollno(participant: Participant) {
  if (teacherId in participant) {
    return particiapant.teacherId;  //if particiapant has teacherId then return it
  }
  return participant.rollno;
}

In above example we are checking if teacherId is property of participant or not and then performing suitable operation.

Summery -

TypeScript itself narrows the type of a variable in conditional branches. Doing a truthly condition check will remove null and undefined from a type. A typeof type guard is used to narrow a union of primitive types. The instanceof type guard is used for narrowing a union of class types. The in type guard is an excellent way of narrowing union of object types.

Conclusion -

Type driven development makes the code more robust and readable. With the help of type narrowing, development at scale becomes easy. I hope this blog will be helpful for learning basics of type narrowing.