7 July

“Using” Keyword in Typescript 5.2

News
Programming

min. read

Reading Time: 7 minutes

The word using commonly known in the C# language has finally  – to the cheering of many programmers – met its arrival in Typescript. The beta phase of Typescript 5.2 was released on June 30, 2023. What are the advantages of the new using function and in which cases will it be useful?

In this article, we will explain dispose mechanics generally and present some use cases of how to apply new using feature. We will also briefly refresh var, let and const features. Let’s get to the subject.

What is var, let and const?

These 3 keywords allow you to declare variables in Typescript (and also in Javascript). Each of them has its own characteristics, which we will review below.

Var

This is basically the most traditional way of declaring variables, and those declared with var are function-scoped variables, meaning that they are only accessible within the function they are defined in.

If declared outside a function, var variables are globally accessible. Importantly, a key feature of var is that it supports hoisting – a mechanism in which variables and function declarations are moved to the top of their containing scope during the compile phase, before the code has been executed. So in a nutshell, a variable can be used before it’s declared.

Let

This keyword allows you to declare block-scoped local variables. It means that the variable is accessible only in the block in which it was declared, as well as any contained sub-blocks. It’s a contrast to var variables, which can sometimes lead to confusing behavior when accessed outside the block in which they are defined. Unlike var, let doesn’t support hoisting.

Const

Similar to let in that const is also block-scoped. However, const is used to declare constants, for example, variables that cannot be re-assigned after they are declared. This doesn’t mean the variable is immutable. For complex types like objects and arrays, the contents can still be modified, but the variable itself can’t be re-assigned. Like let, const doesn’t support hoisting.

Using in C#

The idea of using declarations may be known by you from the C# language. So it’s not a completely new concept. This function provides a comfortable and reliable way to manage resources and ensure their right disposal. By using keyword you can prevent resource leaks, simplify code and improve readability.

In C#, the using declaration is applied in 2 contexts:

  • Namespace declaration

The using keyword is applied to include a namespace in a program. This allows you to use classes, interfaces, enums, and delegates from this namespace without having to fully qualify the name.

For example:

using System;

Now you can use any method or class from the System namespace without any need to write System. before it. Like Console.Write.Line() instead of System.Console.Write.Line().

  • Resource management

The using keyword is also used to define a scope at the end of which an object will be disposed. This is really handy when working with resources which needs explicit release after use such as File IO, database connections, etc.

For example:

  using (StreamReader sr = new StreamReader("file.txt"))
  {
       // Use the StreamReader object here
   }

The StreamReader object sr will be automatically disposed off at the end of the using block, releasing any resources it was holding.

From C# 8.0 and later, you can also use the word using to create declarations, which is a variable declaration that includes a using modifier:

using var sr = new 
StreamReader("file.txt");

This ensures that the sr variable is available for the remainder of the block in which it’s declared. The Dispose method of the object is called at the end of the block. This feature is especially useful for clearer and more readable code when dealing with multiple nested using statements.

Dispose mechanics in general

The Dispose mechanics, also often known as the dispose pattern, is a design pattern in .NET used to release unmanaged resources such as file streams, database connections, etc. It’s mainly used when dealing with an object using a resource that should be cleaned up when the object is no longer needed.

If we go to the general explanation it looks like this:

  • Implementing IDisposable Interface

A class that encapsulates an unmanaged resource should implement the Disposable interface. This interface has only one method called Dispose() that should be implemented by the class.

public class Resource : IDisposable
{
    public void Dispose()
    {
        // release unmanaged resources here
    }
}
  • Using Dispose Method

The Dispose() method is responsible for freeing the resources. Calling the Dispose method when the object is no longer needed is a programmer’s necessity.

Resource myResource = new Resource();
try
{
    // use the resource
}
finally
{
    if (myResource != null)
        myResource.Dispose();
}
  • Using Statement

To simplify the process of calling Dispose, C# provides a using statement that automatically calls Dispose on the object when it’s no longer needed.

using(Resource myResource = new Resource())
{
    // use the resource
} // Dispose is called automatically here
  • Implementing a Finalizer

In case you forget to call the Dispose method, you can implement a finalizer (destructor in C#) in your class that will be called by Garbage Collector when the object is collected. The finalizer should also call Dispose.

~Resource()
{
    Dispose();
}

How did we get to using keyword?

Finally, we are around the using keyword for Typescript 5.2. So far Typescript has had a similar concept with the import keyword intended to bring in other modules or dependencies into the current module.

There is often a need to do some kind of clean-up after creating an object. For example, you may need to close network connections, remove temporary files or just relieve memory.

In short, Typescript 5.2 supports the New Explicit Resource Management feature in ECMAScript. This feature simplify clean-up tasks, like closing connections or deleting files, by making them a core part of Javascript.

Symbol.dispose

Let’s start by introducing Symbol.dispose, a built-in symbol for resource disposal, and a global type Disposable for objects with dispose method. Anything with a function bound to Symbol.dispose will be regarded as a “resource” – “an object with a specific lifetime” – and can be used with the keyword using.

class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

Later we can call these methods.

export function doSomeWork() {
    const file = new TempFile(".some_temp_file");

    try {
        // ...
    }
    finally {
        file[Symbol.dispose]();
    }
}

Moving the clean-up logic to TempFile itself doesn’t give us much. It could be said that we just moved all the clean-up work from the finally block to the method and in fact that was always possible. In turn, having a well-known “name” for this method means that Javascript might build other functions on top of it as well.

And that brings us to the most important feature discussed in this article – using declarations.

Using declarations

This new using keyword allows us to declare new fixed bindings, just like const. The difference, however, is that variables declared with using get the Symbol.dispose method called at the end of the scope.

So the code could be written then like this:

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

There is no try/finally blocks. Well, any that we can see. Functionally, this is exactly what using declarations can do. We don’t have to deal with it anymore.

Symbol.asyncDispose & await using

Now a few words about asynchronous operations, when we need to wait for them to finish before running any other code. For asynchronous operations, there’s Symbol.asyncDispose and accompanying await using declarations. Similar to using declarations, they check whose dispose must be awaited. Typescript also introduces a global type called AsyncDisposable, which describes any object with an asynchronous dispose method.

const getResource = () => ({
  [Symbol.asyncDispose]: async () => {
    await someAsyncFunc();
  },
});
{
  await using resource = getResource();
}

This will await the Symbol.asyncDispose before continuing.

Potential difficulties

  • If you need to define a return type, a full declaration is too long.

Example:

const getFileHandle = async (path: string) : Promise<{
    filehandle: FileHandle;
    [Symbol.asyncDispose]: () => Promise<void>;
}> => {
  const filehandle = await open(path, "r");
  return {
    filehandle,
    [Symbol.asyncDispose]: async () => {
      await filehandle.close();
    },
  };
};

It would be great to be able to create a shortened version of it – maybe it could look like this:

const getFileHandle = async (path: string) : Promise<[Symbol.dispose]FileHandle> => {...}
  • The square brackets at [Symbol.Dispose] may not be obvious to use.

Use cases

  • File handles

Accessing the file system via the node’s file handler can be made much easier thanks to using keyword.

Without using it looks like this:

import { open } from "node:fs/promises";
let filehandle;
try {
  filehandle = await open("thefile.txt", "r");
} finally {
  await filehandle?.close();
}

And with using:

import { open } from "node:fs/promises";
const getFileHandle = async (path: string) => {
  const filehandle = await open(path, "r");
  return {
    filehandle,
    [Symbol.asyncDispose]: async () => {
      await filehandle.close();
    },
  };
};
{
  await using file = getFileHandle("thefile.txt");
  // Do stuff with file.filehandle
} // Automatically disposed!
  • Database connections

As we mentioned before, managing database resources with using is a common issue in C#.

Without using:

const connection = await getDb();
try {
  // Do stuff with connection
} finally {
  await connection.close();
}

And with using:

const getConnection = async () => {
  const connection = await getDb();
  return {
    connection,
    [Symbol.asyncDispose]: async () => {
      await connection.close();
    },
  };
};
{
  await using db = getConnection();
  // Do stuff with db.connection
} // Automatically closed!

Summary

In conclusion, in this article we discussed not only the function of using itself, but also went through some features occurring so far as substitutes for this declaration.

We also know that using will not replace the keywords: var, let and const, because the use of it will be relevant to specific use cases. Equally important – using is actually only reasonable if it has an async dispose function or a dispose function on it.

Await using feature will be useful for resources such as database connections, where you want to ensure that the connection is closed before the program continues.

So the new keyword using, already added to Javascript and Typescript, has made things a lot easier. Be sure to check this out!


Let's talk

SEND THE EMAIL

I agree that my data in this form will be sent to [email protected] and will be read by human beings. We will answer you as soon as possible. If you sent this form by mistake or want to remove your data, you can let us know by sending an email to [email protected]. We will never send you any spam or share your data with third parties.

I agree that my data in this form will be sent to [email protected] and will be read by human beings. We will answer you as soon as possible. If you sent this form by mistake or want to remove your data, you can let us know by sending an email to [email protected]. We will never send you any spam or share your data with third parties.