Closure in TypeScript

Closure in TypeScript

In this article, we will explain closures in TypeScript.

YouTube Video

Closure in TypeScript

What is a Closure?

Closure refers to the ability to retain references to the scope (environment) in which a function was defined, even if the function is called outside that scope. Below, closures will be explained including type annotations.

Simply put, a closure combines a function and the variable environment where the function was defined, allowing access to that environment when the function is called.

Basic Mechanism of Closures

In TypeScript, when a function is defined within another function, it becomes evident that the inner function can access variables of the outer function. Here is a basic example of a closure with type annotations.

 1function outerFunction(): () => void {
 2    let outerVariable: string = "I am from outer function";
 3
 4    function innerFunction(): void {
 5        // The inner function accesses the variable of the outer function
 6        console.log(outerVariable);
 7    }
 8
 9    return innerFunction;
10}
11
12const closure: () => void = outerFunction();
13closure();  // "I am from outer function"
  • outerFunction returns the inner function innerFunction.
  • innerFunction displays the value of the outer function's variable outerVariable.

Uses and Advantages of Closures

Data Encapsulation

Below is an example of a closure with type annotations for encapsulating data.

 1function createCounter(): () => number {
 2    let count: number = 0;
 3
 4    return function (): number {
 5        count += 1;
 6        return count;
 7    };
 8}
 9
10const counter: () => number = createCounter();
11console.log(counter());  // 1
12console.log(counter());  // 2
13console.log(counter());  // 3
  • The createCounter function returns a function that returns a value of type number.
  • The variable count is defined as a number type and is manipulated within the closure.

Higher-Order Functions

Closures are useful when creating higher-order functions. Here, a higher-order function is a function that takes another function as an argument or returns a function as a result. Below is an example of a higher-order function with clear type annotations.

 1function createMultiplier(multiplier: number): (value: number) => number {
 2    return function (value: number): number {
 3        return value * multiplier;
 4    };
 5}
 6
 7const double: (value: number) => number = createMultiplier(2);
 8console.log(double(5));  // 10
 9
10const triple: (value: number) => number = createMultiplier(3);
11console.log(triple(5));  // 15
  • createMultiplier is a higher-order function that creates a function to multiply by the number it receives as an argument.
  • The inner multiplication function also takes a number and returns the result of the multiplication as a number.

Example of Implementing Closures in TypeScript

Let's also look at an example of implementing a range-based counter as a closure.

 1function rangeCounter(min: number, max: number): () => number | string {
 2    let count: number = min;
 3
 4    return function (): number | string {
 5        if (count <= max) {
 6            return count++;
 7        } else {
 8            return `Count has exceeded the maximum value: ${max}`;
 9        }
10    };
11}
12
13const counter: () => number | string = rangeCounter(1, 5);
14
15console.log(counter());  // 1
16console.log(counter());  // 2
17console.log(counter());  // 3
18console.log(counter());  // 4
19console.log(counter());  // 5
20console.log(counter());  // "Count has exceeded the maximum value: 5"
  • The rangeCounter function returns a function that returns either a number or a string.
  • In the internal function, if count exceeds max, it returns a string type message; otherwise, it returns a number type.

Precautions When Using Closures

Potential Memory Leaks from Closures

Closures can retain variables from an external scope, which may sometimes cause memory leaks. Unnecessary closures need to be explicitly released from memory.

 1function createLeak(): () => void {
 2    // Large array consuming significant memory
 3    const largeArray: string[] = new Array(1000000).fill("leak");
 4
 5    // Closure capturing `largeArray`
 6    return function (): void {
 7        console.log(largeArray[0]); // Using the captured array
 8    };
 9}
10
11// Create a closure that holds a reference to the large array
12let leakyFunction = createLeak();
13
14// The large array is not released as `leakyFunction` still references it
15
16// When the object is no longer needed
17leakyFunction = null; // Explicitly remove the reference
  • In this code, the largeArray created inside createLeak is supposed to be released when it goes out of scope, but it isn't because the closure captures largeArray. As long as leakyFunction exists, this unnecessary memory will continue to be retained.
  • When an object or variable is no longer needed, setting its reference to null allows the garbage collector to detect it and release the memory.

Misuse of Closures in Loops

When creating closures inside a loop, there may be issues referring to the same variable. The following example shows a case where the variable i declared with var does not behave correctly.

1for (var i: number = 0; i < 3; i++) {
2    setTimeout((): void => {
3        console.log(i);
4    }, 1000);
5}
6// Output: 3, 3, 3

This code does not yield the desired result because i refers to the value 3 at the end of the loop. To fix this, either use let to separate the scope or use an immediately-invoked function.

1for (let i: number = 0; i < 3; i++) {
2    setTimeout((): void => {
3        console.log(i);
4    }, 1000);
5}
6// Output: 0, 1, 2

By using let, the scope of i is separated for each loop iteration, yielding the expected results.

Summary

In TypeScript, closures can lead to safer and more predictable code by leveraging the type system. Proper use of closures allows for data encapsulation and flexible design of higher-order functions. Additionally, care must be taken with memory management and unintended scope references when using closures.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video