Page Content

Tutorials

What is Type Narrowing in TypeScript? With Examples

Type Narrowing in TypeScript

The method by which TypeScript determines a variable’s more precise type based on runtime checks carried out within the code is known as type narrowing. When a variable has a broad type, like a union type (string | number), TypeScript can temporarily narrow the type within that particular conditional code block by conducting a check (like as confirming if it is a string), providing the functionality and guarantees of the narrower type. Through this procedure, a value’s permitted type is reduced to none of the possible kinds.

TypeScript accomplishes this through flow-based type inference, in which the type checker refines types throughout the code using special type queries and control flow statements.

Type Guards

A Type Guard is a runtime expression that ensures the type of a value inside a specific scope; it is frequently a conditional check. TypeScript is able to ensure type safety in accordance with the underlying JavaScript code patterns thanks to these inspections. Another type inference technique that applies to a variable inside a certain code block is called a type guard.

Narrowing via Conditional Logic

The foundation of type narrowing is conditional logic. TypeScript is intelligent enough to follow along when writers purposefully branch their code according to potential types, guaranteeing that only operations safe for the narrowed type are allowed within that branch.

Standard conditional statements like if, else, and switch are recognized by TypeScript for this analysis. The else block is particularly powerful because if the if condition successfully narrows the type to one possibility, the else block automatically excludes that possibility, narrowing the type to the remainder of the union.

Built-in Type Guards

The typeof operator is a built-in JavaScript construct recognized by TypeScript as a Type Guard, used primarily to distinguish between primitive types: number, string, boolean, and symbol.

TypeScript recognises that the variable’s type inside a conditional block is limited to the evaluated type when typeof is used.

Code Example using

Examine a function created to manage a string or number of the union type:

function processValue(value: string | number): void {
  // Initially, 'value' is string | number 
  if (typeof value === 'string') { 
    // Within this block, TypeScript knows 'value' is a string 
    
    // Attempting to use a number method results in a compile-time error
    // value.toFixed(2); // Error! Property 'toFixed' does not exist on type 'string'. 
    
    console.log(`Processed String: ${value.substr(1)}`); // OK (string operation)
    
  } else {
    // Because the 'if' condition was false, TypeScript narrows 'value' to number 
    
    // Attempting to use a string method results in a compile-time error
    // value.substr(1); // Error! 

    console.log(`Processed Number: ${value.toFixed(2)}`); // OK (number operation) 
  }
}

// Sample Usage
processValue(123.456);
processValue("hello world");

Output:

Processed Number: 123.46
Processed String: ello world

If the variable is still a union type, the type safety vanishes outside of the conditional block. For instance, attempting to use .substr(1) on x outside the if block above would result in an error because there is no guarantee that x is a string.

Another built-in JavaScript feature that TypeScript uses to restrict types according to class inheritance is the instanceof operator. To determine whether an object is an instance of a given type (class), it is utilised. This works well for handling class hierarchies and figuring out the type of an object at runtime.

Code Example using

TypeScript narrows the type inside the if and matching else clauses by using instanceof:

class Foo {
  foo = 123;
  common = 'A';
}
class Bar {
  bar = 456;
  common = 'B';
}
 
function checkType(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(`Found Foo. Exclusive property: ${arg.foo}`); // OK 
    // console.log(arg.bar); // Error! Property 'bar' does not exist on type 'Foo'. 
  } else {
    // TypeScript knows this MUST BE Bar! 
    // console.log(arg.foo); // Error! Property 'foo' does not exist on type 'Bar'. 
    console.log(`Found Bar. Exclusive property: ${arg.bar}`); // OK 
  }
  
  // The common property is available outside the narrowed blocks 
  console.log(`Shared property: ${arg.common}`); 
}

// Sample Usage
checkType(new Foo());
checkType(new Bar());

Output:

Found Foo. Exclusive property: 123
Shared property: A
Found Bar. Exclusive property: 456
Shared property: B

If the if condition succeeds in narrowing the type to Foo, the else block subsequently knows that the only remaining possibility in the union is Bar. The use of instanceof is also seen in try/catch blocks to correctly narrow the type of the caught error object.

Other Narrowing Techniques

Several additional methods are used by TypeScript to filter types according on data structure and property presence:

In Operator ()

The in operator serves as a Type Guard and safely verifies whether a property on an object exists. The type of a variable that is a union of object types can be narrowed by looking for an attribute that is specific to one member:

interface A { x: number; }
interface B { y: string; }

function doStuff(q: A | B) {
  if ('x' in q) {
    // q is narrowed to type A 
    console.log(q.x); 
  } else {
    // q is narrowed to type B 
    console.log(q.y); 
  }
}

Literal Type Guards

The use of discriminated unions is a potent technique for managing union types made up of object structures. This necessitates that every union member possess a single property, known as the discriminant or type tag, that contains a distinct string or numeric literal type.

TypeScript can filter the entire object type by applying a comparison check (===, ==, switch) to this discriminant attribute.

For example, given type Shape = Square | Rectangle where both interface members possess a kind literal property ("square" or "rectangle"):

interface Square { kind: "square"; size: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
type Shape = Square | Rectangle;

function area(s: Shape) {
  if (s.kind === "square") {
    // TypeScript knows 's' must be a Square 
    return s.size * s.size;
  }
  // Otherwise, 's' must be a Rectangle 
  return s.width * s.height;
}

This guarantees that within their corresponding conditional blocks, the appropriate attributes (s.size or s.width/s.height) are securely accessible. When handling actions in systems like as Redux, this technique is really helpful.

Custom User-Defined Type Guards

The built-in typeof and instanceof tests may not be enough to persuade the compiler of the type in situations when the code largely relies on structural typing or does not follow class hierarchies.

You can write User-Defined Type Guard routines for these situations. Despite using a unique return type signature called a Type Predicate, these normal functions carry out runtime logic: someArgumentName is SomeType.

TypeScript ensures (at compilation time) that the guarded argument matches the SomeType inside the conditional block if this function returns true. The Type Predicate provides information for the compile-time analysis, and the function body houses the runtime check mechanism.

Code Example using User-Defined Type Guards

This example shows how to use a special property check in a custom Type Guard to distinguish between two structurally identical interfaces:

interface Foo {
  foo: number;
  common: string;
}
interface Bar {
  bar: number;
  common: string;
}

/**
 * User Defined Type Guard!
 * The type predicate (arg is Foo) informs the TypeScript compiler.
 */
function isFoo(arg: any): arg is Foo {
  // Runtime logic uses the existence of a unique property to differentiate 
  return arg.foo !== undefined;
}

/**
 * Sample usage of the User Defined Type Guard 
 */
function processObjects(arg: Foo | Bar) {
  if (isFoo(arg)) {
    // arg is successfully narrowed to Foo here
    console.log("Is Foo. Accessing unique property:", arg.foo); // OK
    // console.log(arg.bar); // Error: Property 'bar' does not exist on type 'Foo' 
  } else {
    // arg is narrowed to Bar in the else block
    // console.log(arg.foo); // Error: Property 'foo' does not exist on type 'Bar'
    console.log("Is Bar. Accessing unique property:", arg.bar); // OK 
  }
}

// Sample Usage
processObjects({ foo: 123, common: 'shared_A' });
processObjects({ bar: 456, common: 'shared_B' });

Output:

Is Foo. Accessing unique property: 123
Is Bar. Accessing unique property: 456

Instead of inlining laborious typeof or instanceof checks, this technique enables developers to encapsulate complex checking logic into reusable functions, enhancing readability and maintainability. It should be noted that TypeScript will not narrow the type in the falsy direction if a user-defined type guard returns false unless the guard logic naturally exhausts the other options.

Summary of Utility

With the use of several Type Guards, Type Narrowing makes sure that TypeScript’s static analysis is accurate even while interacting with JavaScript’s dynamic nature. By maintaining the type information as accurate as feasible throughout the control flow, this helps identify any problems, promotes the quality of the code, and greatly improves tooling capabilities like code completion.

Index