Keyword using TypeScript: Explicit Resource Management

Foto oleh Pixabay on Pexels
Keyword using mendeklarasikan sebuah resource yang method Symbol.dispose-nya dipanggil otomatis ketika blok pembungkusnya keluar, baik karena selesai normal, sebuah return, maupun error yang dilempar. Ini menggantikan pembersihan try/finally manual sehingga file, koneksi database, dan lock dilepaskan secara deterministik tanpa Anda menulis kode pelepasan.
using memanggil method sinkron Symbol.dispose di akhir scope, sedangkan await using memanggil method asinkron Symbol.asyncDispose dan menunggu promise yang dikembalikannya sebelum melanjutkan. Gunakan await using untuk resource yang pembersihannya asinkron, seperti menutup koneksi database atau mem-flush sebuah stream.
Explicit resource management dengan using dan await using hadir di TypeScript 5.2. Fitur ini mengimplementasikan proposal explicit resource management TC39, yang berada di Stage 3 saat fitur ini dirilis.
Anda harus mengatur target kompilasi ke es2022 atau di bawahnya, dan lib Anda harus menyertakan esnext atau yang lebih sempit esnext.disposable agar tipe Disposable dan AsyncDisposable dapat diselesaikan. Perlu diingat esnext.disposable hanya menambahkan deklarasi tipe, bukan polyfill runtime.
Implementasikan interface Disposable dengan menambahkan method sinkron Symbol.dispose, atau interface AsyncDisposable dengan menambahkan method async Symbol.asyncDispose. Letakkan logika pembersihan Anda di dalam method itu, lalu deklarasikan instance dengan using atau await using dan runtime akan memanggilnya saat keluar scope.

Foto oleh Pixabay on Pexels
Ringkasan Utama
Keyword using di TypeScript, yang hadir sejak TypeScript 5.2, otomatis membuang sebuah resource saat scope-nya berakhir dengan memanggil method Symbol.dispose, sedangkan await using memanggil Symbol.asyncDispose dan menunggunya. Ini menggantikan pembersihan try/finally yang rawan lupa, sehingga koneksi database, file handle, dan lock tertutup secara deterministik bahkan ketika kode melempar error.
Setiap engineer backend pernah mengirim bug yang sama setidaknya sekali: koneksi database, file handle, atau lock yang dibuka tetapi tidak pernah dilepaskan karena satu baris pembersihan terlupa, atau karena sebuah early return melewati blok finally. Resource bocor diam-diam, connection pool kehabisan slot saat beban tinggi, dan insiden baru muncul berjam-jam kemudian ketika gejalanya sudah jauh dari penyebabnya.
TypeScript 5.2 mengambil solusinya dari proposal explicit resource management TC39: deklarasi using dan await using. Keduanya mengikat masa hidup sebuah resource ke blok tempatnya berada, sehingga pembersihan dijamin oleh bahasa, bukan oleh kedisiplinan Anda. Di tulisan ini saya membahas masalahnya, sintaksnya, cara membuat kelas Anda sendiri disposable, dan pengaturan tsconfig persis yang dibutuhkan fitur ini.
Pola klasik untuk pembersihan deterministik adalah try/finally. Anda memperoleh resource, mengerjakan tugas di blok try, lalu melepaskannya di finally agar pelepasan tetap berjalan baik tugasnya sukses maupun melempar error. Cara ini bekerja, tetapi seluruh bebannya ada di penulis kode: setiap pengambilan butuh finally yang sepadan, dan setiap jalur keluar lebih awal harus melewatinya.
Berikut bentuknya dengan koneksi Postgres. Blok finally itulah yang mengerjakan pengamanan sesungguhnya, dan justru baris itulah yang paling mungkin hilang saat refactor atau saat sebuah early return ditambahkan.
async function loadUsers() {
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
try {
const { rows } = await client.query("SELECT id, email FROM users LIMIT 10");
return rows;
} finally {
// Forget this line and the connection leaks until the pool starves.
await client.end();
}
}Deklarasi using mengikat sebuah resource ke blok saat ini. Ketika kendali meninggalkan blok itu dengan alasan apa pun, entah selesai normal, sebuah return, atau error yang dilempar, runtime otomatis memanggil method Symbol.dispose milik resource tersebut. Tidak ada finally yang perlu ditulis dan tidak ada jalur keluar yang terlewat. Untuk pembersihan asinkron Anda menulis await using, yang mencari Symbol.asyncDispose dan menunggu promise yang dikembalikan sebelum melanjutkan.
Pembuangan mengikuti urutan last-in-first-out, persis seperti tumpukan: jika Anda mendeklarasikan dua resource dalam satu blok, yang dideklarasikan kedua dibuang lebih dulu. Ini sesuai dengan cara resource bersarang biasanya saling bergantung, dan sama dengan urutan yang diberikan blok try/finally bersarang, tanpa perlu bersarang.
using dan await using adalah deklarasi ber-scope blok. Keduanya bekerja di dalam fungsi, loop, dan blok if, tetapi tidak bisa muncul di level teratas sebuah skrip klasik. Gunakan DisposableStack atau method defer ketika Anda perlu mendaftarkan pembersihan untuk resource yang sendirinya tidak mengimplementasikan interface disposable.
Sebuah resource ikut serta dengan mengimplementasikan salah satu dari dua interface. Disposable membutuhkan method Symbol.dispose yang sinkron; AsyncDisposable membutuhkan method Symbol.asyncDispose yang async. Apa pun yang membungkus I/O, sebuah koneksi, file, stream, atau lock, adalah kandidat alami. Berikut sebuah pembungkus koneksi Postgres yang menutup dirinya sendiri lewat await using.
import { Client } from "pg";
// A resource opts in by implementing AsyncDisposable:
// it must define an async [Symbol.asyncDispose]() method.
class ManagedConnection implements AsyncDisposable {
private constructor(private readonly client: Client) {}
static async connect(url: string): Promise<ManagedConnection> {
const client = new Client({ connectionString: url });
await client.connect();
return new ManagedConnection(client);
}
query(text: string, params?: unknown[]) {
return this.client.query(text, params);
}
async [Symbol.asyncDispose](): Promise<void> {
await this.client.end();
}
}
async function loadUsers() {
// "await using" awaits [Symbol.asyncDispose] when the block exits.
await using db = await ManagedConnection.connect(process.env.DATABASE_URL!);
const { rows } = await db.query("SELECT id, email FROM users LIMIT 10");
return rows;
// db.client.end() runs here automatically -- even if the query throws.
}Perhatikan apa yang tidak perlu lagi dilakukan pemanggilnya. Tidak ada finally, tidak ada pemanggilan end secara manual, dan tidak ada cara untuk lupa membersihkan, karena saat blok keluar, runtime menunggu Symbol.asyncDispose untuk Anda. Jika query melempar error, pembuangan tetap berjalan; jika dua pembuangan sama-sama melempar error, runtime memunculkan SuppressedError yang menyimpan rujukan ke keduanya sehingga tidak ada yang tertelan diam-diam.
Fitur ini hadir di TypeScript 5.2. Untuk mengompilasinya Anda harus mengatur target ke es2022 atau di bawahnya, dan lib Anda harus menyertakan esnext atau yang lebih sempit esnext.disposable agar tipe Disposable dan AsyncDisposable dapat diselesaikan. Konfigurasi minimalnya tampak seperti ini.
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.disposable", "dom"]
}
}Karena sebagian besar runtime belum mendukung well-known symbol tersebut saat fitur ini rilis, TypeScript menurunkannya menjadi logika try/finally yang setara dan mengharapkan polyfill untuk Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack, dan SuppressedError. Sisi engine kini sudah menyusul: V8 merilis explicit resource management di v13.8 dan Chromium 134, dan runtime JavaScript modern kini mendukung sintaksnya secara native.
esnext.disposable hanya menambahkan deklarasi tipe, ia tidak menambahkan polyfill runtime. Jika target deployment Anda tidak memiliki dukungan native, Anda tetap membutuhkan polyfill Symbol.dispose dan Symbol.asyncDispose, atau library helper yang menyediakannya, atau blok using akan melempar error saat runtime meski build-nya lolos.
Keduanya bukan saingan, melainkan yang satu menggantikan boilerplate yang lain. try/finally adalah mekanisme manual yang harus Anda rakit dengan benar setiap kali; using adalah jaminan yang sama yang dipindahkan ke dalam deklarasinya sendiri. Tabel berikut merangkum di mana perbedaannya benar-benar terasa.
| Aspek | try / finally | using / await using |
|---|---|---|
| Pemicu pembersihan | Anda memanggil pelepasan secara manual di finally | Runtime memanggil Symbol.dispose di akhir scope |
| Lupa membersihkan | Kebocoran diam-diam, kompiler tetap bungkam | Mustahil terjadi begitu resource dideklarasikan dengan using |
| Banyak resource | Blok bersarang atau pengurutan manual yang cermat | Satu baris masing-masing, dibuang dalam urutan last-in-first-out |
| Pembersihan asinkron | await di dalam finally, mudah terlewat | await using menunggu Symbol.asyncDispose untuk Anda |
| Return awal atau throw | Setiap jalur keluar harus melewati finally | Pembuangan terjadi di setiap jalur keluar secara otomatis |
Pedoman saya sederhana: jika sebuah nilai memiliki sesuatu yang harus dilepaskan, berilah ia Symbol.dispose atau Symbol.asyncDispose dan serahkan dengan using. Anda berhenti menulis kode pembersihan dan mulai mendeklarasikan kepemilikan, dan seluruh kelas kebocoran akibat finally yang terlupa lenyap dari basis kode Anda.