using
keywordWhen 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:
using
operator as a solution for better resource managementusing
operator in TypeScriptasync
operations?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:
We don’t really need to manage this resource for various reasons:
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.
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
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(); }
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.
using
operator as a solution for better resource managementEnter 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')"); }
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
operator in TypeScriptFortunately, 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" }, }
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
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:
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}`); } }
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}`); } }
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')"); }
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
The order of these operations is important. To summarize:
using
statement are createdusing
statement goes out of scope, the dispose
function is called on objects created via the using
statementNote 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.
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"); } }
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
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 {}
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Resource management in TypeScript with the <code>using</code> keyword"
Nice detailed Information on Resource Management