ECMAScript Explicit Resource Management early implementation in TypeScript 5.2

By Mohi Bagherani

4 min read

The TypeScript implementation of the `using` and `await using` declarations from the TC39 Explicit Resource Management proposal, which is currently at Stage 3

ECMAScript Explicit Resource Management early implementation in TypeScript 5.2
Authors

ECMAScript Explicit Resource Management early implementation in TypeScript 5.2

This article on "ECMAScript Explicit Resource Management Early Implementation in TypeScript 5.2" is for developers who are interested in managing resources explicitly in their JavaScript programs. It explains how this feature works and how it can simplify resource management code.

Explicit Resource Management indicates a system whereby the lifetime of a “resource” is managed explicitly by the user. This can be done either imperatively (by directly calling a method like Symbol.dispose) or declaratively (through a block-scoped declaration like using).

A "Resource" is something that has a specific lifespan. When that lifespan is over, the program needs to perform a specific action, like closing a file or freeing up memory, so that the program can continue running smoothly. Examples of resources include file handles and network sockets.

At the moment of writing, the ECMAScript proposal for Explicit Resource Management is at stage 3 which means it will be launched within a few months. However, the TypeScript community has already implemented it in TypeScript version 5.2.

This implementation will add a feature to the JavaScript language similar to other languages such as C# using syntax [link], Python with syntax [link], or try [link] in Java.

// C# example of the 'using' statement
using (var file = File.OpenRead(“path-to-file”))
{
    // use the file
}
// file.dispose() method has been called now to release resources.

In general, without using this new feature, a conventional free-up pattern is by using the try/finally syntax:

var obj
try {
  obj = someResource()
  // ...
} finally {
  obj.release() // or any other clean-up method provided by the resource
}

Now this can be written in this way in TypeScript 5.2:

using obj = someResource();
// ...

When the program's execution goes outside of the block that the obj has been defined, the object will no longer be needed and the dispose method will be called.

TypeScript introduced an interface called Disposable that can be used to implement the using statement. To do this, your class should implement the Disposable interface, which requires the class to have a [Symbol.dispose] method. This method is responsible for freeing up any resources used by the class when it is no longer needed. By implementing the Disposable interface, the TypeScript compiler knows how to translate the using syntax into a try/finally block that the current JavaScript engine can understand.

class MyResource implements Disposable {
    [Symbol.dispose]() {
      // clean-up logic
    }
}

using obj = new MyResource();
// ...

Or, if you have a function instead of a class, your function should return an object that has the Symbol.dispose method:

function myResource(): Disposable {
    return {
        [Symbol.dispose](){ /* clean-up logic */}
    }
}

using obj = myResource();

Good to know that after compilation, the above code will become something like this:

class MyResource {
  [Symbol.dispose]() {
    // clean-up logic
  }
}
var obj
const env_1 = { stack: [], error: void 0, hasError: false }
try {
  obj = __addDisposableResource(env_1, MyResource(), false)
} catch (e_1) {
  env_1.error = e_1
  env_1.hasError = true
} finally {
  __disposeResources(env_1)
}

As you can see here the TypeScript compiler has compiled the using statement into the traditional try/finally pattern that I mentioned before and of course, with some simple functions to manage the allocated resources(__addDisposableResource and __disposeResources will be generated by the compiler in the output file).

Nested scopes and using

Objects can be created in the nested scopes:

function work() {
    using a = resource();
    {
        using b = resource();
    }
}

work();
// b.dispose()
// a.dispose()

In this case, after leaving the scope, the dispose methods will be called respectively.

DisposableStack class

Implementing the Symbol.dispose method can be a good pattern to use when you have a class or function that needs to be disposed of at some point. However, there are a few potential problems to consider. Firstly, implementing the dispose method can add unnecessary abstraction to your code in some cases, which can make it more difficult to read and understand. Secondly, many existing libraries and modules may not have implemented the dispose method yet, in which case you will need to create a wrapper around them and implement the clean-up code (dispose) yourself. Keep these considerations in mind when deciding whether or not to implement the dispose method in your code.

Here is an example from the TypeScript blog:

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);
    }
}

function doSomeWork() {
    using file = new TempFile("path-to-file");
}

This class has been implemented (with all of its sophistication that it might have later) to only have a Symbol.dispose method that would be called by the engine. However, TypeScript 5.2 introduces the DisposableStack class, which makes it simpler to use Explicit Resource Management.

Here is how:

function doSomeWork() {
    const path = "path-to-file";
    const file = fs.openSync(path, "w+");

    using cleanup = new DisposableStack();
    cleanup.defer(() => {
        fs.closeSync(file);
        fs.unlinkSync(path);
    });

    // use file...
}

Here we’ve created an instance of DisposableStack right after opening the file; It has the defer method that accepts a callback function which is suitable for clean-up that will be invoked at the moment of disposing the cleanup object.
After the above code is compiled, the resulting JavaScript code will include a finally block. This finally block will call the callback function that was passed to the defer method. This ensures that the callback function is always executed, even if an error occurs in the try block.

Async dispose method

Sometimes for your dispose logic you might need to use asynchronous operations using async/await. In this case, you can simply use the await before the using keyword:

await using file = OpenFile('...');

In this case, in the OpenFile function or class, you should implement the dispose method using Symbol.asyncDispose like below:

import * as fs from 'fs'

function OpenFile(path): AsyncDisposable {
  const file = fs.open(path)

  return {
    file,
    async [Symbol.asyncDispose]() {
      await fs.anAsyncOperation()
    },
  }
}

Polyfills

Because this feature is so recent, most runtimes will not support it natively. To use it, you will need runtime polyfills for the following:

  • Symbol.dispose
  • Symbol.asyncDispose
  • DisposableStack
  • AsyncDisposableStack
  • SuppressedError

Symbol.dispose and Symbol.asyncDispose can be polyfilled like this:

Symbol.dispose ??= Symbol('Symbol.dispose')
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose')

The compilation target in the tsconfig file should be es2022 or below and the library setting to either include "esnext" or "esnext.disposable"

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022", "esnext.disposable", "dom"]
  }
}

For more information on this feature, take a look at the work on GitHub!


Share