DEV Community

Cover image for Using using in TypeScript for resource management
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Using using in TypeScript for resource management

Written by Lewis Cianci✏️

When it comes to software development, everyone is most interested in what the actual end product can do. How many people plan to use the app every day? What is its marketing potential?

The big picture — the end product’s usefulness and profitability — is what gets the most attention. However, in reality, producing and shipping an app like that is the result of making lots of high-quality, yet comparatively small decisions along the way.

Investing in thoughtful strategy early on leads to maintainable applications that are less likely to experience weird bugs or problems when scaled up. One crucial strategic area is resource management, which is a seemingly small but immensely important aspect of software development.

In this article, we will discuss the importance of resource management and how TypeScript’s new using operator can help us manage our resources better.

Jump ahead:

What is resource management?

We program within finite environments, with finite amounts of processing power and memory. To that end, we can’t forego resource management.

Resource management helps us avoid taking up unlimited amounts of memory and processing power. Without proper resource management, our apps would definitely run slowly and eventually crash.

Within software, whenever we want to do something, we call a function. Sometimes, the lifetime of the object that we are calling the function on is static — in other words, it only exists once in memory and can be called at any time, for any reason.

An example of a function like this could be if we were to call console.log('log output') in our developer console within Chrome or Edge. To do this, we don’t even need to construct a new console object. It’s already globally available within our browser window: Screenshot Of Developer Console Showing A Simple Example Of A Console Object To Demonstrate Calling A Function In Software

We don’t really need to manage this resource for various reasons:

  • It’s created outside of our control
  • It exists as a globally accessible function
  • It’s also completed within nanoseconds
  • It doesn’t run asynchronously

But not all of our apps are this simple. With modern web apps, we can do grand things — call web services on the other side of the world, send our images for AI analysis, write detailed responses to a database — the list goes on.

However, in doing so, we make heavy use of system resources. As a result, it’s up to us to appropriately manage these resources as time goes on.

The problem with current resource management strategies

Let’s imagine that we want to connect to a database, write some records, and then disconnect from the database. This would probably equate to roughly the following:

let db = new DbConnection("server=localhost,token=<...token here...>");
await db.connect();
await db.execute("INSERT INTO testusers ('Rob')");
await db.close(); // clean up used resourceso
Enter fullscreen mode Exit fullscreen mode

We’ve instantiated our DbConnection, connected to it, and have also inserted some data. Because these objects have been created, they are now taking up some memory on the host system.

After this takes place, we need to clean up after ourselves, which involves disconnecting from the database and indicating that the system can dispose of the DbConnection object. Depending on where you are using this code, you might use something like db.close() or db.dispose() on the following line.

Assuming everything works correctly, we’ll connect to the database, the data will be inserted, and the resources will get cleaned up. But, as we know in software development, a number of things can go wrong in a normal environment, including shaky connections, server crashes, and more.

If something goes wrong, we can just use a try...catch block to handle issues and perform the appropriate cleanup actions, like so:

let db = new DbConnection("server=localhost,token=<...token here...>");
try{
  await db.connect();
  await db.execute("INSERT INTO testusers ('Rob')");
}
finally
{
  await db.close();
}
Enter fullscreen mode Exit fullscreen mode

But even in this case, our code still isn’t technically correct.

Our db object would be in a closed state because the code within the finally block has been executed. Later on in this file, we might forget that we’ve already closed this object and attempt to call db again, only to have our app throw an exception.

These issues increase if we have other objects that depend on the DbConnection, which would need to be disposed of first before we can dispose of the underlying objects. While this is certainly not impossible to do at the moment, it can make our code noisier.

The using operator as a solution for better resource management

Enter the explicit resource management proposal, which describes — among many other things — a new using operator that was introduced in TypeScript 5.2 and is making its way into JavaScript. From the top of the README file, here’s what this proposal aims to do:

This proposal intends to address a common pattern in software development regarding the lifetime and management of various resources (memory, I/O, etc.). This pattern generally includes the allocation of a resource and the ability to explicitly release critical resources.

It sounds good, but what does it mean? Well, it’s a little complicated, so let’s use common concepts to understand what this change means to us.

Consider the humble constructor in TypeScript. Whenever we instantiate an object within TypeScript, the constructor runs as the object is “constructed.” We’re always guaranteed that the constructor will run when an object is created.

After the object is constructed and we have carried out the appropriate operation, we likely have some teardown to do — cleaning up resources, closing connections, etc. How do we do that? With TypeScript 5.2, we can use the using operator.

Imagine that we have a table insert within a function. Let’s see how we can write this out with the using operator:

function insertTestUsers() {
  using db = DbConnection("server=localhost,token=<...token here...>");
  await db.connect();
  await db.execute("INSERT INTO testusers ('Rob')");
}
Enter fullscreen mode Exit fullscreen mode

By now, if you’re like me, you’ve been using TypeScript for quite some time. You probably already know most of the syntactical keywords that you’re likely to use in your day-to-day. But the using keyword sticks out as new here.

If you’ve ever used C#, you may recognize using and already understand that it leads to much easier resource management and cleanup. Since C# and TypeScript are pretty closely related in terms of who invented them, it’s perhaps no surprise to see similar keywords being used for similar functions.

But that’s in C# — how do we use it in TypeScript? Well, as of 22 August 2023, it’s currently available in the new TypeScript 5.2, so you should be able to use it today, as long as you update your installed version of TypeScript!

Using the using operator in TypeScript

Fortunately, setting up to use the using operator is quite simple. Start by creating a new directory, adding a new tsconfig.json to the directory, and pasting the following within the file:

{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.disposable", "dom"],
        "module": "es2022"
    },
}
Enter fullscreen mode Exit fullscreen mode

If you’re like me, you might get a squiggly line under the ESNext.Disposable entry. It’s okay to ignore it.

Next, within the directory, run the following command:

npm -i typescript --dev
Enter fullscreen mode Exit fullscreen mode

If you run tsc --version, it should return TypeScript 5.2. That means you’re good to go 🎉

Now, let’s create a couple of classes:

  • One class will be responsible for writing to the database
  • The other class will be responsible for writing to the log, which is in the database. The logger will have a dependency on the database

We’ll also include an import to core-js so we can use the dispose functionality before it becomes widely available:

import 'core-js/actual/index.js'

class FakeDatabaseWriter {
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect(){
        this.connected = true;
    }
}
class FakeLogger {
    constructor(private db: FakeDatabaseWriter){
    }
    writeLogMessage(message: string){
        console.log(`Pretending to log ${message}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, for the secret sauce of this tutorial! Let’s make both of these classes implement Disposable and then implement the requirements of this interface, which is a new function called Symbol.dispose.

Within these functions, let’s carry out some logging that describes when these objects are constructed and when they are disposed of for the example we’re using. Our code now looks like this:

// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");

class FakeDatabaseWriter implements Disposable {
    constructor(){
        console.log("The FakeDatabaseWriter is being constructed");
    }
    [Symbol.dispose](): void {
        console.log("The FakeDatabaseWriter is disposing! Setting this.connected to false.");
        this.connected = false;
        console.log(`Connected property is now ${this.connected}`);
    }
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect(){
        this.connected = true;
    }
}
class FakeLogger implements Disposable {

    [Symbol.dispose](): void {
        console.log("The FakeLogger is disposing!");
    }
    constructor(private db: FakeDatabaseWriter){
        console.log("The FakeLogger is being constructed");
    }

    writeLogMessage(message: string){
        console.log(`Pretending to log ${message}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

With all of this now set up, let's create our classes and see what happens.

We can use the new using keyword to instantiate our FakeDatabaseWriter and then immediately also instantiate a new FakeLogger with the already-created database writer. Then, we can connect and execute the statement as required:

{
    using db = new FakeDatabaseWriter(), logger = new FakeLogger(db);
    db.connect();
    db.executeSQLStatement("INSERT INTO fakeTable VALUES ('value one', 'value two')");
}
Enter fullscreen mode Exit fullscreen mode

Now run npm run exec. The output is as follows:

The FakeDatabaseWriter is being constructed
The FakeLogger is being constructed
Pretending to execute statement INSERT INTO fakeTable VALUES ('value one', 'value two')
The FakeLogger is disposing!
The FakeDatabaseWriter is disposing! Setting this.connected to false.
Connected property is now false
Enter fullscreen mode Exit fullscreen mode

The order of these operations is important. To summarize:

  1. Objects in the using statement are created
  2. Code is executed on subsequent lines
  3. When the using statement goes out of scope, the dispose function is called on objects created via the using statement

Note that the disposals are called in reverse order. This is key! By calling the disposals in reverse order, dependent objects are disposed of first, before other underlying objects.

The result of this is that everything is constructed and disposed of cleanly, without having to remember to do so in try...catch...finally blocks.

What about resource management for async operations?

Our previous code samples in this article concerned functions that run entirely synchronously, or block the calling function. For functions that complete extremely quickly, this is perfectly acceptable.

The reality, however, is that we are very likely to encounter operations that will require an asynchronous cleanup as we wait for resources to be freed up. In these cases, we can implement AsyncDisposable, like this:

class LongRunningCleanup implements AsyncDisposable{
    async [Symbol.asyncDispose]()  {
        console.log(`LongRunningCleanup is disposing! Began at ${new Date()}`)
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(`LongRunningCleanup is finished disposing. Finished at ${new Date()}`)
    }
    async longRunningOperation() {
        console.log("Executing long running operation...");
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("Long running operation has finished");
    }
}
Enter fullscreen mode Exit fullscreen mode

Executing this code has the following results:

The FakeDatabaseWriter is being constructed
The FakeLogger is being constructed
Pretending to execute statement INSERT INTO fakeTable VALUES ('value one', 'value two')
Executing long running operation...
LongRunningCleanup is disposing! Began at Mon Jul 31 2023 23:09:34 GMT+1000 (Australian Eastern Standard Time)
Long running operation has finished
LongRunningCleanup is finished disposing. Finished at Mon Jul 31 2023 23:09:35 GMT+1000 (Australian Eastern Standard Time)
The FakeLogger is disposing!
The FakeDatabaseWriter is disposing! Setting this.connected to false.
Connected property is now false
Enter fullscreen mode Exit fullscreen mode

Again, we see the dependencies are freed in reverse order. The difference here is that the call to the asynchronous disposal method is called and the await is observed.

The complete code sample is this:

// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");

class FakeDatabaseWriter implements Disposable {
    constructor() {
        console.log("The FakeDatabaseWriter is being constructed");
    }
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect() {
        this.connected = true;
    }
    [Symbol.dispose]() {
        console.log("The FakeDatabaseWriter is disposing! Setting this.connected to false.");
        this.connected = false;
        console.log(`Connected property is now ${this.connected}`);
    }
}
class FakeLogger implements Disposable {
    [Symbol.dispose]()  {
        console.log("The FakeLogger is disposing!");
    }
    constructor(private db: FakeDatabaseWriter) {
        console.log("The FakeLogger is being constructed");
    }

    writeLogMessage(message: string) {
        console.log(`Pretending to log ${message}`);
    }
}
class LongRunningCleanup implements AsyncDisposable{
    async [Symbol.asyncDispose]()  {
        console.log(`LongRunningCleanup is disposing! Began at ${new Date()}`)
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(`LongRunningCleanup is finished disposing. Finished at ${new Date()}`)
    }
    async longRunningOperation() {
        console.log("Executing long running operation...");
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("Long running operation has finished");
    }
}

using db = new FakeDatabaseWriter(), logger = new FakeLogger(db);
await using longrunning = new LongRunningCleanup();
db.connect();
db.executeSQLStatement("INSERT INTO fakeTable VALUES ('value one', 'value two')");
longrunning.longRunningOperation();

export {}
Enter fullscreen mode Exit fullscreen mode

Final thoughts: TypeScript and the future of resource management

Using the new using statement should make resource management in TypeScript much easier, and you won’t have to keep remembering to call dispose manually. Goodbye, memory leaks!

At the same time, this change doesn’t cause any regressions or break backward compatibility, so you can hopefully start using it without worrying too much. Enjoy your streamlined resource management! 😎🏢


LogRocket: Full visibility into your web and mobile apps

LogRocket Signup

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Try it for free.

Top comments (0)