Skip to main content
BlogSoftware Development

Keyword using TypeScript: Explicit Resource Management

Bagaimana keyword using dan await using di TypeScript otomatis menutup koneksi DB, file, dan lock di akhir scope lewat Symbol.dispose dan Symbol.asyncDispose.

Matthews Wong

Software Engineer

Agustus 20269 menit baca
Keyword using TypeScript: Explicit Resource Management

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.

Masalahnya: pembersihan yang harus Anda ingat

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

Solusinya: using membuang di akhir scope

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.

Membuat kelas Anda sendiri 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.

Versi TypeScript dan syarat tsconfig

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.

using versus try/finally, berdampingan

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.

Aspektry / finallyusing / await using
Pemicu pembersihanAnda memanggil pelepasan secara manual di finallyRuntime memanggil Symbol.dispose di akhir scope
Lupa membersihkanKebocoran diam-diam, kompiler tetap bungkamMustahil terjadi begitu resource dideklarasikan dengan using
Banyak resourceBlok bersarang atau pengurutan manual yang cermatSatu baris masing-masing, dibuang dalam urutan last-in-first-out
Pembersihan asinkronawait di dalam finally, mudah terlewatawait using menunggu Symbol.asyncDispose untuk Anda
Return awal atau throwSetiap jalur keluar harus melewati finallyPembuangan 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.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa fungsi keyword using di TypeScript?

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.

Apa perbedaan antara using dan await using?

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.

Versi TypeScript mana yang menambahkan keyword using?

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.

Pengaturan tsconfig apa yang dibutuhkan keyword using?

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.

Bagaimana cara membuat kelas saya sendiri bekerja dengan keyword using?

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.