Mert Özen Her Satırda Daha İleri
Frontend TypeScript İleri — #09

TypeScript'te using ve await using: Symbol.dispose ile Kaynak Yönetimi

Mert Özen 3 Kas 2026 7 dk 1 görüntülenme
TypeScript'te using ve await using: Symbol.dispose ile Kaynak Yönetimi

TS 5.2 ile gelen using ve await using'in TypeScript tarafını görüyoruz: Disposable type'ları, gereken lib ayarı, derleyicinin kontrolleri, DisposableStack ve tsc'nin ürettiği JavaScript'in Node ile Safari'de ne anlama geldiği.

Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.

Önce Tahmin Et

İki kaynak açıyoruz, sonra bloğun ortasında bir hata fırlatıyoruz:

function track(name: string): Disposable {
  console.log(`open ${name}`);
  return {
    [Symbol.dispose]() {
      console.log(`close ${name}`);
    },
  };
}

try {
  using db = track("db");
  using cache = track("cache");
  throw new Error("boom");
} catch (error) {
  if (error instanceof Error) {
    console.log(`caught ${error.message}`);
  }
}

Konsola ne yazar, hangi sırayla? close satırları hiç görünür mü, görünürse caught boom'dan önce mi sonra mı? Bir de şunu düşün: bu dosyayı serinin standart ayarlarıyla derlemeye çalışsan tsc ne der? Cevapları sırayla vereceğiz.

using Neyi Çözüyor?

JavaScript serilerinin son yazısında Explicit Resource Management önerisini ve using'i görmüştük. Kısaca hatırlayalım: bir dosya, veritabanı bağlantısı ya da zamanlayıcı açtığında onu kapatmayı unutmamak için try/finally yazarsın. İki kaynakta iç içe iki try, üç kaynakta üç. using bu işi dile taşıyor: using ile tanımladığın değişken, bulunduğu blok nasıl biterse bitsin, normal dönüşle de hatayla da, kendiliğinden kapatılır. Kapatmayı object'in Symbol.dispose method'u yapar.

TypeScript bu syntax'ı TS 5.2'den beri destekliyor. Bu yazıda JavaScript tarafını tekrar anlatmayacağız; TypeScript'in eklediklerine bakacağız: Disposable ve AsyncDisposable type'ları, derleyicinin yaptığı kontroller ve tsc'nin ürettiği JavaScript. Bu sonuncusu, Safari'yi düşündüğümüzde özellikle önemli.

Baştaki sorunun ilk cevabı: konsola sırasıyla open db, open cache, close cache, close db ve caught boom yazılır. Kaynaklar açıldıkları sıranın tersiyle kapanır ve bu kapanış, hata catch'e ulaşmadan önce olur. Blok bir hatayla bitse bile kaynaklar sızmaz.

Önce lib Ayarı

Sorunun ikinci kısmı: bu seride kodları --lib es2025,dom ile denetliyoruz. Bu ayarla yukarıdaki kod derlenmez. tsc'nin ilk söylediği şu:

error TS2318: Cannot find global type 'Disposable'.

Ardından Symbol.dispose için TS2550 gelir ve derleyici lib'i esnext'e çekmeyi önerir. Sebep, using'in henüz ES2025'in parçası olmaması; tipleri esnext.disposable adlı ayrı bir lib dosyasında duruyor. Tek bir using nothing = null; satırı bile, using'in arkasındaki global Disposable type'ına ihtiyaç duyduğu için aynı TS2318'i verir. Çözüm, bu lib'i eklemek:

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

İstersen doğrudan "esnext" de yazabilirsin; o zaman henüz standartlaşmamış diğer API'lerin tipleri de gelir. Sadece ihtiyacın olanı eklemek, hangi özelliğin yeni olduğunu tsconfig'e bakan herkese açıkça söyler. Bu yazının geri kalanındaki örnekleri bu lib ayarıyla denetledik.

Disposable Type'ı ve Derleyicinin Kontrolleri

Disposable çok küçük bir interface: tek bir [Symbol.dispose](): void method'u. TypeScript, using'e verdiğin değerin bu interface'e uyduğunu ya da null veya undefined olduğunu denetler. Kapatma method'unun adını yanlış yazarsan ya da eski alışkanlıkla close dersen kod çalışmadan yakalanır:

const connection = {
  close() {
    console.log("closed");
  },
};

function query() {
  using conn = connection; // Hata
}
error TS2741: Property '[Symbol.dispose]' is missing in type '{ close(): void; }' but required in type 'Disposable'.

Düz JavaScript'te bu hata ancak çalışma anında, TypeError olarak ortaya çıkardı. Kendi kaynak class'larını yazarken implements Disposable eklemek de iyi bir alışkanlık; method'u unutursan class tanımında hata alırsın:

class TempFile implements Disposable {
  path: string;
  constructor(path: string) {
    this.path = path;
    console.log(`create ${path}`);
  }

  write(text: string) {
    console.log(`write "${text}" to ${this.path}`);
  }

  [Symbol.dispose]() {
    console.log(`delete ${this.path}`);
  }
}

function exportReport() {
  using file = new TempFile("/tmp/report.csv");
  file.write("id,total");
  return "exported";
}

console.log(exportReport());
// create /tmp/report.csv
// write "id,total" to /tmp/report.csv
// delete /tmp/report.csv
// exported

Çıktının sırasına dikkat et: delete, fonksiyon return ettikten sonra ama çağıran kod dönüş değerini yazdırmadan önce çalışıyor. using değişkeni const gibi davranır; file = ... diye yeniden atamaya kalkarsan TS2588 ile "Cannot assign to 'file' because it is a constant." hatasını alırsın.

await using ve AsyncDisposable

Bazı kaynaklar senkron kapanamaz: bağlantıyı kapatmadan önce sunucuya bir mesaj göndermen ya da tampondaki veriyi diske yazman gerekir. Bunun için Symbol.asyncDispose ve await using var. TypeScript'teki karşılığı AsyncDisposable interface'i:

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms));
}

async function connect(url: string): Promise<AsyncDisposable> {
  await wait(10);
  console.log(`connected to ${url}`);
  return {
    async [Symbol.asyncDispose]() {
      await wait(10);
      console.log(`disconnected from ${url}`);
    },
  };
}

async function sync() {
  await using conn = await connect("db://main");
  console.log("syncing");
}

sync().then(() => console.log("done"));
// connected to db://main
// syncing
// disconnected from db://main
// done

Satırdaki iki await farklı işler yapıyor. Sağdaki await connect(...) bağlantının açılmasını bekliyor; soldaki await using ise bloğun sonunda kapatmanın bitmesini bekleyeceğimizi söylüyor. sync'in döndürdüğü promise, bağlantı kapanana kadar tamamlanmaz; done bu yüzden en sonda. await using'e Symbol.dispose'u olan senkron bir kaynak da verebilirsin, tersi ise olmaz.

DisposableStack ile Birden Fazla Kaynak

Kaynağın kendi Symbol.dispose'u yoksa ya da kaynak sayısı baştan belli değilse DisposableStack kullanırsın. JavaScript tarafını görmüştük; TypeScript'te method'ların hepsi tiplidir. Örneğin adopt, verdiğin değerin type'ını kapatma fonksiyonuna aynen aktarır:

function track(name: string): Disposable {
  console.log(`open ${name}`);
  return {
    [Symbol.dispose]() {
      console.log(`close ${name}`);
    },
  };
}

function startPolling(): DisposableStack {
  using stack = new DisposableStack();
  stack.adopt(setInterval(() => {}, 1000), (id) => {
    clearInterval(id);
    console.log("timer cleared");
  });
  stack.defer(() => console.log("polling stopped"));
  stack.use(track("socket"));
  return stack.move();
}

{
  using polling = startPolling();
  console.log("polling...");
}
// open socket
// polling...
// close socket
// polling stopped
// timer cleared

adopt'un callback'inde id'nin type'ını yazmadık; TypeScript onu setInterval'in dönüş tipinden çıkardı. Fonksiyonun sonundaki stack.move() deseni de dikkat çekici: kurulum yarıda bir hatayla kesilirse using stack o ana kadar eklenen her şeyi temizler. Her şey yolunda giderse move() kaynakları yeni bir stack'e aktarır, eski stack boşalır ve fonksiyondan çıkarken kapatacak bir şeyi kalmaz. Kapatma sorumluluğu artık çağıran kodda.

Derleyici Ne Üretir? Node ve Safari

using TypeScript'e özgü bir syntax değil, JavaScript syntax'ı. Tipler silindiğinde geriye geçerli JavaScript kalır; yani decorator'ların aksine erasable sayılır. Node 24 Explicit Resource Management'ı varsayılan olarak açtı, bu yüzden Node 26 bu yazıdaki dosyaları node file.ts ile doğrudan çalıştırır. Daha eski bir Node'da ise motor using'i tanımaz; yerel Node 22.2 ile denediğimizde target: esnext ile derlenmiş çıktı using satırında SyntaxError verdi.

Burada tsc'nin dönüşümü devreye giriyor. target es2025 olduğunda, ki TypeScript 7'nin varsayılanı budur, tsc using'i eski syntax'a çevirir. exportReport'un çıktısı şöyle:

function exportReport() {
  const env_1 = { stack: [], error: void 0, hasError: false };
  try {
    const file = __addDisposableResource(env_1, new TempFile("/tmp/report.csv"), false);
    file.write("id,total");
    return "exported";
  }
  catch (e_1) {
    env_1.error = e_1;
    env_1.hasError = true;
  }
  finally {
    __disposeResources(env_1);
  }
}

Tanıdık bir try/finally, artı dosyanın başına eklenen iki yardımcı fonksiyon. Bu çıktı aynı Node 22.2'de sorunsuz çalıştı. Safari açısından anlamı şu: JavaScript serisinde gördüğümüz gibi Safari using'i henüz desteklemiyor ve bu syntax'ı içeren bir script'i hiç ayrıştıramıyor. tsc ile es2025'e derlediğinde syntax sorunu ortadan kalkar. Ama runtime sorunu kalır: __addDisposableResource içinde Symbol.dispose'un var olup olmadığı kontrol ediliyor ve yoksa TypeError("Symbol.dispose is not defined.") fırlatılıyor. DisposableStack'i ise tsc hiç dönüştürmez, sadece tipini bilir. Safari kullanıcıların varsa bu ikisi için bir polyfill yüklemen gerekir. SuppressedError için yardımcı fonksiyon kendi yedeğini getiriyor.

Sık Düşülen Tuzaklar

Birincisi, kaynağı using ile tanımlayıp fonksiyondan döndürmek. Değer çağırana ulaştığında kaynak çoktan kapanmıştır. Kaynağı dışarı vermek istiyorsan ya using kullanma ya da DisposableStack ve move() desenine geç.

İkincisi, destructuring yapmaya çalışmak. using { length } = resource; yazarsan tsc TS1492 ile "'using' declarations may not have binding patterns." der. Kaynağı bir değişkende tut, property'lerini sonra oku.

Üçüncüsü, await using'i async olmayan bir fonksiyonda kullanmak. tsc TS2852 ile bunun yalnızca async fonksiyonlarda ve module'lerin en üst seviyesinde mümkün olduğunu söyler.

Dördüncüsü, lib'i değiştirdin diye runtime'ın da hazır olduğunu sanmak. esnext.disposable sadece tipleri getirir. Kodun nerede çalışacağına bak: Node 26 ve güncel Chrome ya da Firefox'ta sorun yok, Safari'de polyfill ve es2025 hedefi gerekir.

Küçük Bir Deneme

★☆☆ Derlenir mi, tahmin et: function f() { using x = 42; }. Hata verirse mesajı oku ve hangi değerlere izin verildiğini not et; sonra 42 yerine undefined yaz ve tekrar dene.

★★☆ TempFile'ı await using ile kullanılacak bir AsyncTempFile'a çevir: Symbol.asyncDispose içinde 10 ms bekleyip dosyayı silsin. implements AsyncDisposable yaz ve method'u kasıtlı olarak Symbol.dispose diye adlandırınca derleyicinin ne dediğini gör.

★★★ Bu yazıdaki exportReport örneğini --target es2025 ve --target esnext ile ayrı ayrı derle ve iki çıktıyı karşılaştır. Hangisini Safari'de çalıştırabilirsin, çalıştırmak için başka neye ihtiyacın olur?

Son iki yazıda çalışma zamanına dokunan özelliklerle uğraştık. Bir sonraki yazıda tamamen tip seviyesine dönüyoruz: type-challenges'ın kolay seviye bulmacalarını çözerek, bu seride öğrendiğimiz conditional, mapped ve recursive type'ları bir arada kullanacağız.