Back to Blog
TypeScriptusingDisposableResource Management

TypeScript using Keyword Guide

Master the TypeScript using keyword for automatic resource disposal and cleanup patterns.

B
Bootspring Team
Engineering
October 22, 2019
6 min read

The using keyword enables automatic resource disposal when leaving scope. Here's how to use it effectively.

Basic Usage#

1// Disposable interface 2interface Disposable { 3 [Symbol.dispose](): void; 4} 5 6// Create disposable resource 7function createResource(): Disposable { 8 console.log('Resource acquired'); 9 10 return { 11 [Symbol.dispose]() { 12 console.log('Resource disposed'); 13 }, 14 }; 15} 16 17// Using declaration 18function example() { 19 using resource = createResource(); 20 // Resource acquired 21 22 console.log('Using resource...'); 23 24 // Resource automatically disposed when scope exits 25} 26// Resource disposed 27 28example();

File Handling#

1import { open } from 'fs/promises'; 2 3// File handle with disposal 4async function readFile(path: string) { 5 await using file = await open(path, 'r'); 6 7 const content = await file.readFile('utf-8'); 8 console.log(content); 9 10 // File handle automatically closed 11} 12 13// Multiple files 14async function copyFile(src: string, dest: string) { 15 await using srcFile = await open(src, 'r'); 16 await using destFile = await open(dest, 'w'); 17 18 const content = await srcFile.readFile(); 19 await destFile.writeFile(content); 20 21 // Both files closed in reverse order 22}

Database Connections#

1class DatabaseConnection implements Disposable { 2 private connected = false; 3 4 async connect() { 5 console.log('Connecting to database...'); 6 this.connected = true; 7 return this; 8 } 9 10 query(sql: string) { 11 if (!this.connected) throw new Error('Not connected'); 12 console.log(`Executing: ${sql}`); 13 return []; 14 } 15 16 [Symbol.dispose]() { 17 if (this.connected) { 18 console.log('Closing database connection'); 19 this.connected = false; 20 } 21 } 22} 23 24async function getUserData(userId: number) { 25 using db = await new DatabaseConnection().connect(); 26 27 const user = db.query(`SELECT * FROM users WHERE id = ${userId}`); 28 29 // Connection closed automatically 30 return user; 31}

Lock Management#

1class Lock implements Disposable { 2 private static locks = new Map<string, Lock>(); 3 private released = false; 4 5 private constructor(private name: string) {} 6 7 static acquire(name: string): Lock { 8 if (this.locks.has(name)) { 9 throw new Error(`Lock ${name} already held`); 10 } 11 12 const lock = new Lock(name); 13 this.locks.set(name, lock); 14 console.log(`Lock ${name} acquired`); 15 return lock; 16 } 17 18 [Symbol.dispose]() { 19 if (!this.released) { 20 Lock.locks.delete(this.name); 21 this.released = true; 22 console.log(`Lock ${this.name} released`); 23 } 24 } 25} 26 27function criticalSection() { 28 using lock = Lock.acquire('resource'); 29 30 // Protected code 31 console.log('Doing critical work...'); 32 33 // Lock released automatically 34}

Async Disposable#

1interface AsyncDisposable { 2 [Symbol.asyncDispose](): Promise<void>; 3} 4 5class AsyncResource implements AsyncDisposable { 6 constructor(private name: string) { 7 console.log(`${name} created`); 8 } 9 10 async [Symbol.asyncDispose]() { 11 console.log(`${this.name} disposing...`); 12 await new Promise((resolve) => setTimeout(resolve, 100)); 13 console.log(`${this.name} disposed`); 14 } 15} 16 17async function useAsyncResource() { 18 await using resource = new AsyncResource('MyResource'); 19 20 console.log('Using async resource...'); 21 22 // Async disposal awaited 23}

Transaction Pattern#

1class Transaction implements Disposable { 2 private committed = false; 3 private rolledBack = false; 4 5 constructor(private db: Database) { 6 db.execute('BEGIN TRANSACTION'); 7 console.log('Transaction started'); 8 } 9 10 execute(sql: string) { 11 if (this.committed || this.rolledBack) { 12 throw new Error('Transaction already ended'); 13 } 14 return this.db.execute(sql); 15 } 16 17 commit() { 18 if (!this.committed && !this.rolledBack) { 19 this.db.execute('COMMIT'); 20 this.committed = true; 21 console.log('Transaction committed'); 22 } 23 } 24 25 [Symbol.dispose]() { 26 if (!this.committed && !this.rolledBack) { 27 this.db.execute('ROLLBACK'); 28 this.rolledBack = true; 29 console.log('Transaction rolled back'); 30 } 31 } 32} 33 34function transferMoney(from: string, to: string, amount: number) { 35 using tx = new Transaction(db); 36 37 tx.execute(`UPDATE accounts SET balance = balance - ${amount} WHERE id = '${from}'`); 38 tx.execute(`UPDATE accounts SET balance = balance + ${amount} WHERE id = '${to}'`); 39 40 // Only commit if no errors 41 tx.commit(); 42 43 // If error thrown, transaction auto-rolled back 44}

Timer Cleanup#

1class Timer implements Disposable { 2 private intervalId: NodeJS.Timeout | null = null; 3 4 start(callback: () => void, interval: number) { 5 this.intervalId = setInterval(callback, interval); 6 console.log('Timer started'); 7 return this; 8 } 9 10 [Symbol.dispose]() { 11 if (this.intervalId) { 12 clearInterval(this.intervalId); 13 this.intervalId = null; 14 console.log('Timer stopped'); 15 } 16 } 17} 18 19function monitorStatus() { 20 using timer = new Timer().start(() => { 21 console.log('Checking status...'); 22 }, 1000); 23 24 // Do work for 5 seconds 25 // Timer automatically cleaned up 26}

Event Listener Cleanup#

1class EventSubscription implements Disposable { 2 constructor( 3 private target: EventTarget, 4 private type: string, 5 private handler: EventListener 6 ) { 7 target.addEventListener(type, handler); 8 console.log(`Subscribed to ${type}`); 9 } 10 11 [Symbol.dispose]() { 12 this.target.removeEventListener(this.type, this.handler); 13 console.log(`Unsubscribed from ${this.type}`); 14 } 15} 16 17function setupClickHandler(element: HTMLElement) { 18 using subscription = new EventSubscription(element, 'click', (e) => { 19 console.log('Clicked!', e); 20 }); 21 22 // Do something... 23 24 // Listener removed when scope exits 25}

DisposableStack#

1// Manage multiple disposables 2class DisposableStack implements Disposable { 3 private disposables: Disposable[] = []; 4 5 use<T extends Disposable>(disposable: T): T { 6 this.disposables.push(disposable); 7 return disposable; 8 } 9 10 defer(cleanup: () => void) { 11 this.disposables.push({ 12 [Symbol.dispose]: cleanup, 13 }); 14 } 15 16 [Symbol.dispose]() { 17 // Dispose in reverse order 18 while (this.disposables.length > 0) { 19 const disposable = this.disposables.pop()!; 20 disposable[Symbol.dispose](); 21 } 22 } 23} 24 25function complexOperation() { 26 using stack = new DisposableStack(); 27 28 const conn1 = stack.use(new DatabaseConnection()); 29 const conn2 = stack.use(new DatabaseConnection()); 30 31 stack.defer(() => { 32 console.log('Additional cleanup'); 33 }); 34 35 // All resources disposed in reverse order 36}

HTTP Client#

1class HttpClient implements AsyncDisposable { 2 private abortController = new AbortController(); 3 4 async fetch(url: string) { 5 const response = await fetch(url, { 6 signal: this.abortController.signal, 7 }); 8 return response.json(); 9 } 10 11 async [Symbol.asyncDispose]() { 12 this.abortController.abort(); 13 console.log('HTTP client disposed, pending requests cancelled'); 14 } 15} 16 17async function fetchData() { 18 await using client = new HttpClient(); 19 20 const data = await client.fetch('/api/data'); 21 console.log(data); 22 23 // Any pending requests cancelled on disposal 24}

Temporary Directory#

1import { mkdtemp, rm } from 'fs/promises'; 2import { tmpdir } from 'os'; 3import { join } from 'path'; 4 5class TempDirectory implements AsyncDisposable { 6 private constructor(public readonly path: string) {} 7 8 static async create(): Promise<TempDirectory> { 9 const path = await mkdtemp(join(tmpdir(), 'app-')); 10 console.log(`Created temp directory: ${path}`); 11 return new TempDirectory(path); 12 } 13 14 async [Symbol.asyncDispose]() { 15 await rm(this.path, { recursive: true, force: true }); 16 console.log(`Removed temp directory: ${this.path}`); 17 } 18} 19 20async function processFiles() { 21 await using tempDir = await TempDirectory.create(); 22 23 // Use tempDir.path for temporary files 24 console.log(`Working in: ${tempDir.path}`); 25 26 // Directory automatically cleaned up 27}

Best Practices#

Usage: ✓ Use for resource cleanup ✓ Implement Symbol.dispose ✓ Use await using for async ✓ Keep dispose idempotent Patterns: ✓ File handles ✓ Database connections ✓ Locks and transactions ✓ Event subscriptions Benefits: ✓ Automatic cleanup ✓ Exception safety ✓ Cleaner code ✓ No try/finally needed Avoid: ✗ Long-lived resources ✗ Complex dispose logic ✗ Side effects in dispose ✗ Forgetting async disposal

Conclusion#

The using keyword provides automatic resource disposal in TypeScript. Implement Symbol.dispose for synchronous cleanup or Symbol.asyncDispose for async cleanup. Resources are disposed in reverse order of acquisition, and disposal happens even when exceptions occur. Use DisposableStack for managing multiple related resources.

Share this article

Help spread the word about Bootspring