Mert Özen Her Satırda Daha İleri
Frontend JavaScript İleri — #16

JavaScript ve Ufuktaki Özellikler: TC39 Aşamaları, Temporal ve using

Mert Özen 3 Kas 2026 10 dk 5 görüntülenme
JavaScript ve Ufuktaki Özellikler: TC39 Aşamaları, Temporal ve using

Bir özelliğin TC39 aşamalarından geçip dile girişini, Date'in sorunlarını çözen Temporal'ı (PlainDate, ZonedDateTime, Duration) ve using ile kaynak yönetimini Safari desteği notlarıyla öğreniyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Daha önce gördüğümüz Date ile basit bir iş yapıyoruz: 31 Ocak'tan bir ay sonrasını bulmak.

const start = new Date(2026, 0, 31); // 31 Ocak 2026
const next = start;
next.setMonth(1); // bir ay sonrası

console.log(start.getMonth(), start.getDate());

Bu kod konsola ne yazdırır? Dikkat et, start'ı yazdırıyoruz, next'ni değil. Okumaya devam etmeden önce bir tahmin yap. Cevabı Temporal'ın bu sorunu nasıl çözdüğünü görürken vereceğiz.

Bir Özellik Dile Nasıl Girer? TC39 Aşamaları

Serinin ilk yazısında JavaScript'in TC39 adlı bir komite tarafından yıllık sürümlerle geliştirildiğini söylemiştik. Artık o sürecin içine bakabiliriz. Her yeni özellik bir öneri (proposal) olarak doğar ve aşamalardan geçer. Bunu bir yemeğin restoran menüsüne girmesine benzetebilirsin.

Aşama 0'da fikir sadece aşçının aklındadır. Aşama 1'de fikri savunan bir "champion" vardır ve komite problemi incelemeyi kabul etmiştir. Aşama 2'de çözüm seçilmiş, tarif kâğıda yazılmıştır ama ayrıntılar hâlâ değişebilir. Sonradan eklenen aşama 2.7'de tarif tamamdır ve tadıma geçilir: şartname metni bitmiş, Test262 adlı resmî test paketine testler yazılmıştır. Aşama 3'te yemek birkaç şubenin deneme menüsündedir; tarayıcılar özelliği uygulamaya başlar. Aşama 4'te yemek ana menüdedir: en az iki motorda uyumlu uygulaması yapılıp testleri geçmiştir ve özellik bir sonraki yıllık sürüme girer.

Yıllık sürüm her yıl haziranda onaylanır; ES2026'nın onayı Haziran 2026'daydı. Bu yazının iki yıldızı, Temporal ve Explicit Resource Management, 2026'da 4. aşamaya ulaştı ama ES2026'ya yetişmedi; ES2027'de yer almaları bekleniyor.

Temporal: Date'in Yerine Gelen

Baştaki sorunun cevabı 2 3, yani 3 Mart. Üç ayrı sürpriz üst üste bindi. Birincisi, next = start kopya almadı; ikisi aynı object'i gösteriyor ve setMonth o object'i yerinde değiştirdi. İkincisi, aylar 0'dan başlıyor; 1 Şubat demek, ekrana gelen 2 de Mart. Üçüncüsü, 31 Şubat diye bir gün olmadığı için Date taşan üç günü sessizce Mart'a aktardı. Buna sadece yerel saat ile UTC'yi tanımasını ve "sadece tarih" diye bir tipinin olmamasını da ekle.

Temporal bu sorunları baştan çözmek için tasarlandı. Bütün değerleri değişmez (immutable); her işlem yeni bir object döndürür. Aylar 1'den başlar. Saat dilimleri "Europe/Istanbul" gibi adlarla açıkça belirtilir. En önemlisi, her iş için ayrı bir tip vardır. Date her işi yapmaya çalışan bir çakı gibiyse, Temporal düzenli bir alet çantasıdır:

Temporal.PlainDate duvar takvimindeki bir gündür, saati ve saat dilimi yoktur; doğum günü gibi. Temporal.PlainTime kol saatindeki saattir, "her gün 09:00" gibi. Temporal.PlainDateTime ikisinin birleşimidir ama hâlâ saat dilimsizdir. Temporal.ZonedDateTime uçak biletindeki kalkış saatidir: tarih, saat ve saat dilimi bir arada, dünyada tek bir ana karşılık gelir. Temporal.Instant zaman çizgisindeki kesin bir nokta, Temporal.Duration da "2 saat 30 dakika" gibi bir süredir. Şimdiki zaman için Temporal.Now var.

Bunu Chrome ya da Firefox'un konsoluna yapıştırıp dene:

const date = Temporal.PlainDate.from("2026-01-31");
const oneMonthLater = date.add({ months: 1 });

console.log(oneMonthLater.toString()); // "2026-02-28"
console.log(date.toString());          // "2026-01-31", değişmedi
console.log(date.month);               // 1

const today = Temporal.Now.plainDateISO();
console.log(today.add({ days: 1 }).toString()); // yarının tarihi

const first = Temporal.PlainDate.from("2026-01-01");
const last = Temporal.PlainDate.from("2026-03-15");
console.log(first.until(last).toString());                           // "P73D"
console.log(first.until(last, { largestUnit: "month" }).toString()); // "P2M14D"
console.log(Temporal.PlainDate.compare(first, last));                // -1
console.log(date.toLocaleString("tr-TR", { dateStyle: "long" })); // "31 Ocak 2026"

31 Ocak'a bir ay eklediğimizde Temporal Mart'a taşmaz, Şubat'ın son gününde durur. Bu varsayılan davranışın adı overflow: "constrain"; taşmayı hata olarak görmek istersen { overflow: "reject" } verirsin ve bir RangeError alırsın. date değişmeden kaldı, çünkü add yeni bir object döndürdü. until iki tarih arasındaki farkı bir Duration olarak verir; P73D ISO 8601 biçiminde "73 gün" demek. Varsayılan en büyük birim gündür; ay ve gün olarak istersen largestUnit ile söylersin. Biçimlendirme için daha önce gördüğümüz Intl aynen çalışıyor.

ZonedDateTime ve Duration

Saat dilimleri, Date'in en çok can yaktığı yerdi. Orta seride gördüğümüz yaz saati tuzağını hatırla. Almanya'da 29 Mart 2026'da saatler bir saat ileri alınıyor. Temporal'a bunu soralım:

const berlin = Temporal.ZonedDateTime.from("2026-03-28T12:00[Europe/Berlin]");

console.log(berlin.add({ days: 1 }).toString());
// "2026-03-29T12:00:00+02:00[Europe/Berlin]"
console.log(berlin.add({ hours: 24 }).toString());
// "2026-03-29T13:00:00+02:00[Europe/Berlin]"

console.log(berlin.withTimeZone("Europe/Istanbul").toString());
// "2026-03-28T14:00:00+03:00[Europe/Istanbul]"

"Bir gün sonra" ile "24 saat sonra" farklı sonuç veriyor ve ikisi de doğru. Bir gün eklemek takvimde ertesi güne, aynı saate gitmektir. 24 saat eklemek ise gerçekten 24 saatin geçmesi; o gece bir saat kaybolduğu için duvar saati 13:00'ü gösteriyor. Temporal hangisini istediğini açıkça söyletiyor. withTimeZone aynı anı başka bir dilimden gösterir; Türkiye 2016'dan beri yaz saati uygulamadığı için İstanbul hep +03:00. Şimdiki zamanı bir dilimde almak için Temporal.Now.zonedDateTimeISO("Europe/Istanbul"), tarayıcının dilimi için Temporal.Now.timeZoneId() var.

Süreler için Duration var:

const duration = Temporal.Duration.from({ hours: 1, minutes: 30 });
console.log(duration.toString());                 // "PT1H30M"
console.log(duration.total({ unit: "minutes" })); // 90

const meeting = Temporal.PlainTime.from("09:45");
console.log(meeting.add(duration).toString());    // "11:15:00"

const longDuration = Temporal.Duration.from({ minutes: 150 });
console.log(longDuration.round({ largestUnit: "hours" }).toString()); // "PT2H30M"

Süreyi milisaniye cinsinden çıplak bir sayı yerine bir değer olarak taşımak, kodu okuyanın "bu 5400000 ne?" diye sormasını engeller. total süreyi tek birime çevirir, round okunur birimlere dağıtır. Eski kodla köprü kurmak gerektiğinde new Date().toTemporalInstant() bir Date'i Instant'a çevirir.

using: Kaynakları Kendiliğinden Kapatmak

Programlamada açtığın bazı şeyleri mutlaka kapatman gerekir: bir bağlantı, bir zamanlayıcı, bir event listener, bir observer. Bunun klasik yolu try/finally:

const connection = openConnection();
try {
  connection.send("hello");
} finally {
  connection.close();
}

Çalışır ama gürültülüdür; iki kaynakta iç içe iki try yazarsın. Explicit Resource Management bu işi dile taşıyor: using ile tanımladığın değişken, bulunduğu blok nasıl biterse bitsin, kendiliğinden kapatılır. Kapatma işini object'in Symbol.dispose method'u yapar. Konsolda dene:

function resource(name) {
  console.log(`${name} opened`);
  return {
    [Symbol.dispose]() {
      console.log(`${name} closed`);
    },
  };
}

{
  using a = resource("A");
  using b = resource("B");
  console.log("work done");
}
// A opened, B opened, work done, B closed, A closed

Kapatma, açılışın ters sırasıyla yapılır. Akşam evden çıkarken önce ışığı söndürüp sonra kapıyı kilitlemen gibi; son açtığın, ilk kapattığındır. Bu sıra önemlidir, çünkü sonra açılan kaynak çoğu zaman öncekine bağlıdır.

Kurallar const'a benziyor: değişken tanımlanırken değer almalı ve yeniden atanamaz. Değer ya Symbol.dispose method'u olan bir object ya da null veya undefined olmalı; başka bir şey verirsen TypeError alırsın. Blok bir hatayla biterse kapatma yine yapılır. Kapatma sırasında da hata çıkarsa ikisi bir SuppressedError içinde birleşir: error alanında sonraki, suppressed alanında önceki hata durur.

Gerçek hayattan küçük bir örnek, daha önce performans yazısında gördüğümüz performance.now ile bir ölçüm aracı:

function measure(label) {
  const start = performance.now();
  return {
    [Symbol.dispose]() {
      console.log(`${label}: ${(performance.now() - start).toFixed(1)} ms`);
    },
  };
}

function heavyWork() {
  using _ = measure("heavyWork");
  let total = 0;
  for (let i = 0; i < 1e6; i++) total += i;
  return total; // fonksiyon nereden çıkarsa çıksın ölçüm yazılır
}

heavyWork();

DisposableStack ve await using

Kaynakların kendi Symbol.dispose method'u yoksa ya da sayıları baştan belli değilse DisposableStack bir temizlik stack'i sunar. use() disposable bir object, adopt(value, fn) disposable olmayan bir değeri kapatma fonksiyonuyla, defer(fn) sadece bir fonksiyon ekler. dispose() hepsini ters sırayla çalıştırır, disposed kapatılıp kapatılmadığını söyler, move() da içeriği yeni bir stack'e taşıyıp eskisini boşaltır:

function setup() {
  using stack = new DisposableStack();

  const timer = setInterval(() => console.log("tick"), 1000);
  stack.defer(() => clearInterval(timer));

  const observer = new ResizeObserver(() => console.log("size changed"));
  observer.observe(document.body);
  stack.defer(() => observer.disconnect());

  return stack.move(); // her şey yolundaysa temizlik sorumluluğu çağırana geçer
}

const cleanup = setup();
// Daha sonra, bu bölümle işimiz bitince:
cleanup.dispose();

Desen ilk bakışta tuhaf gelebilir ama çok akıllıca. setup çalışırken bir hata çıkarsa using o ana kadar kurulan her şeyi temizler; yarım kalan kurulum arkasında çalışan bir zamanlayıcı bırakmaz. Her şey yolunda giderse move() kaynakları yeni bir stack'e aktarır ve boşalan eski stack'in kapatacak bir şeyi kalmaz. Observer yazısındaki "temizlik fonksiyonu döndür" alışkanlığının dile gömülmüş hâli.

Kapatma işi asenkronsa, mesela veriyi sunucuya göndermeden kapanamayan bir kayıt, Symbol.asyncDispose ve async bir fonksiyon içinde await using kullanırsın; stack hâli de AsyncDisposableStack:

async function run() {
  await using writer = {
    async [Symbol.asyncDispose]() {
      await new Promise((resolve) => setTimeout(resolve, 500));
      console.log("writer closed");
    },
  };
  console.log("working");
}

await run();
console.log("done");
// working, writer closed, done

Bugün Kullanmak: Destek ve Polyfill

Temporal Chrome ve Edge 144, Firefox 139 ile geldi. using ve await using Chrome ve Edge 134, Firefox 141'den beri çalışıyor. Safari ise ikisini de henüz desteklemiyor, bu yüzden ikisi de Baseline'da "Limited" görünüyor.

Temporal'da durum görece kolay, çünkü o bir API; eksikse yerine aynı şeyi yapan bir polyfill script'i koyabilirsin. Önce var olup olmadığını kontrol et:

if (typeof Temporal === "undefined") {
  // Safari: bir Temporal polyfill'i yükle ya da Date ve Intl ile devam et
}

using ise bir syntax, yani bir anahtar kelime ve syntax polyfill ile eklenemez. Daha da önemlisi, Safari using içeren bir script'i baştan sona ayrıştıramaz ve dosyadaki hiçbir satır çalışmaz. Symbol.dispose ve DisposableStack polyfill'le eklenebilir; anahtar kelime için ise kodu eski syntax'a çeviren araçlar gerekir ve onlar bu serinin konusu değil. Safari kullanıcıların varsa şimdilik DisposableStack'i polyfill'iyle ve try/finally içinde elle kullan. Syntax desteğini çalışma anında öğrenmek için küçük bir hile var:

let usingVar = true;
try {
  new Function("{ using x = null; }");
} catch {
  usingVar = false;
}
console.log("using supported?", usingVar);

new Function verdiğin metni o anda ayrıştırır; destek yoksa SyntaxError fırlatır ve ana script çalışmaya devam eder. Desteğe göre farklı bir module'ü daha önce gördüğümüz dynamic import() ile yükleyebilirsin.

ES2027 ve Sonrası

2026'da 4. aşamaya ulaşıp ES2027'ye girmesi beklenen başka özellikler de var ve çoğu daha önce gördüğümüz iterator helpers'ın devamı: iterator'ları sabit boyutlu parçalara bölmek (chunking), iterator'larda includes ve join, birden çok iterator'ı birlikte gezen Iterator.zip:

const names = ["Ada", "Alan", "Grace"];
const scores = [92, 78, 85];

for (const [name, score] of Iterator.zip([names, scores])) {
  console.log(`${name}: ${score}`);
}

Bunlar şimdilik sadece en yeni Chrome ve Firefox sürümlerinde var, Safari'de yok. Listede bir de Atomics.pause duruyor; Web Worker'lar arasında paylaşılan bellekte kısa süreli bekleme yapan kodlar için işlemciye "biraz nefes al" ipucu veriyor.

Bir özelliği ne zaman gönül rahatlığıyla kullanabileceğini anlamak için iki yere bakman yeterli: TC39'un GitHub'daki proposals deposu aşamasını, MDN sayfalarındaki Baseline rozeti de tarayıcı desteğini söyler.

Sık Düşülen Tuzaklar

Temporal object'leri değişmez olduğu için en sık hata, sonucu bir yere atamayı unutmak. date.add({ days: 1 }) tek başına hiçbir şeyi değiştirmez; const tomorrow = date.add({ days: 1 }) yazman gerekir.

Temporal object'lerini === ile karşılaştırmak da işe yaramaz; aynı günü gösteren iki PlainDate iki ayrı object'tir ve false döner. date.equals(other) ya da Temporal.PlainDate.compare(a, b) kullan.

Yanlış tipi seçmek daha sinsi bir hata. Bir uçuşun kalkış saatini PlainDateTime ile tutarsan saat dilimi kaybolur ve başka ülkedeki kullanıcıya yanlış saat gösterirsin. Belli bir ana bağlı her şey için ZonedDateTime ya da Instant kullan. Yaz saati geçişindeki boşluğa da dikkat: Berlin'de 2026-03-29T02:30 diye bir an yok, ZonedDateTime.from onu varsayılan olarak 03:30'a kaydırır.

using tarafında ilk tuzak yer: normal bir script'in en üst seviyesinde using kullanamazsın, SyntaxError alırsın. Bir bloğun, fonksiyonun ya da module'ün içinde olmalısın; örneklerimizi bu yüzden süslü parantezlere sardık. switch'in doğrudan içinde ve for...in başlığında da kullanılamaz. İkinci tuzak closure: bir callback using değişkenini yakalarsa blok bittikten sonra çağrıldığında kaynağı çoktan kapanmış bulur.

Küçük Bir Deneme

★☆☆ Temporal ile bir sonraki doğum gününe kaç gün kaldığını hesapla. Temporal.Now.plainDateISO() ile bugünü al, with({ month: 5, day: 19 }) ile bu yılki doğum gününü bul, geçtiyse add({ years: 1 }) ile bir sonrakine geç ve until ile farkı gün olarak yazdır.

★★☆ Çalıştırmadan önce çıktıyı ve sırasını tahmin et, sonra Chrome ya da Firefox'un konsolunda dene:

try {
  using a = { [Symbol.dispose]() { console.log("a"); } };
  using b = { [Symbol.dispose]() { console.log("b"); } };
  console.log("c");
  throw new Error("d");
} catch (error) {
  console.log(error.message);
}

★★★ tempListener(target, type, fn) adında bir fonksiyon yaz. addEventListener çağırsın ve Symbol.dispose ile o listener'ı kaldıran bir object döndürsün. Bir blokta using ile kullan ve bloktan çıkınca tıklamaların artık dinlenmediğini doğrula. Sonra üç farklı listener'ı tek bir DisposableStack'te topla ve hepsini tek dispose() çağrısıyla kaldır.

JavaScript her yaz yeni bir sürümle büyümeyi sürdürüyor ve bu seriler de onunla birlikte büyüyecek. ES2027 onaylandığında ve sıradaki öneriler 4. aşamaya ulaştıkça yeni yazılarla döneceğiz. O zamana kadar karşına yeni bir özellik çıktığında önce aşamasına, sonra Baseline durumuna bakmayı alışkanlık hâline getir.