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 functioninnerFunction
.innerFunction
displays the value of the outer function's variableouterVariable
.
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 typenumber
. - The variable
count
is defined as anumber
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 anumber
or astring
. - In the internal function, if
count
exceedsmax
, it returns astring
type message; otherwise, it returns anumber
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 insidecreateLeak
is supposed to be released when it goes out of scope, but it isn't because the closure captureslargeArray
. As long asleakyFunction
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.