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

TypeScript Enum, Namespace ve Parameter Properties: Erasable Olmayan Syntax

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript Enum, Namespace ve Parameter Properties: Erasable Olmayan Syntax

Numeric ve string enum'ları, const enum'u, namespace'i ve parameter properties'i derlenmiş çıktılarıyla inceliyor; neden erasable olmadıklarını, erasableSyntaxOnly'yi ve as const alternatifini görüyoruz.

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

Önce Tahmin Et

Dört yönü olan basit bir enum tanımlıyoruz ve değerlerini konsola yazdırıyoruz:

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

console.log(Object.values(Direction));

Konsola kaç element'li bir array yazar ve içinde ne vardır? Dört yön, dört değer diye düşünüyorsan bir kez daha bak. Bir ipucu: TypeScript'te yazdığın tiplerin neredeyse tamamı derlemede silinir, ama bu kod öyle değil.

Erasable Ne Demek?

Serinin başından beri örnekleri tek bir kurala göre yazıyoruz: tipleri silersen geriye geçerli bir JavaScript kalmalı. Buna erasable (silinebilir) syntax diyoruz. Node 26 node file.ts dediğinde tam olarak bunu yapar: dosyadaki TypeScript syntax'ını boşluklarla değiştirir ve kalanı çalıştırır. Tip denetimi yapmaz, yeni kod da üretmez.

TypeScript'in birkaç eski özelliği bu kurala uymaz. Enum, çalışma zamanı kodu içeren namespace, parameter properties ve import x = require(...) gibi import = tanımları JavaScript'te karşılığı olmayan, derleyicinin yeni kod üreterek çevirdiği syntax'lardır. Node bunlarla karşılaşınca dosyayı çalıştırmaz ve ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX hatası verir. Eskiden bunları dönüştüren --experimental-transform-types bayrağı Node 26'da kaldırıldı. Yani bu yazıdaki örnekleri Node'da çalıştırmak için önce tsc ile JavaScript'e derlemen gerekir; Playground'da ise doğrudan deneyebilirsin. Decorator'lar da bu gruba giriyor, onları İleri seride ayrıca ele alacağız.

Bu syntax'ları bilmen gerekiyor, çünkü mevcut kod tabanlarında ve library'lerde çok yaygınlar. Ama yeni kodda neden çoğu zaman tercih edilmediklerini de göreceğiz.

Enum: Numeric ve String

Numeric enum'da değer vermezsen üyeler 0'dan başlayarak numaralanır:

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

function fromNumber(n: number): Direction {
  return n;
}

console.log(Direction.Down); // 1
console.log(Direction[1]); // Down
console.log(fromNumber(99)); // 99

// Hata: Type '42' is not assignable to type 'Direction'.
const invalid: Direction = 42;

Direction hem bir type hem de bir değer. tsc onu şu JavaScript'e çevirir:

var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
  Direction[Direction["Left"] = 2] = "Left";
  Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

Her satır iki iş yapıyor: Direction["Up"] = 0 isimden sayıya, dıştaki atama da sayıdan isme bir key ekliyor. Buna reverse mapping denir ve Direction[1]'in "Down" döndürmesinin sebebi bu. Baştaki sorunun cevabı da burada: object'te sekiz key var, Object.values da [ 'Up', 'Down', 'Left', 'Right', 0, 1, 2, 3 ] döndürür.

Örnekteki iki satıra daha dikkat et. TS 5.0'dan beri enum'da karşılığı olmayan bir sayı literal'ını atamak hata veriyor: 42 reddedildi. Ama number tipindeki bir değer hâlâ numeric enum'a atanabiliyor; fromNumber(99) hiçbir uyarı almadan 99 döndürdü. Numeric enum'un tip güvenliği sandığından daha gevşek.

String enum'da her üyeye kendin değer verirsin ve reverse mapping üretilmez:

enum Status {
  Active = "ACTIVE",
  Banned = "BANNED",
}

function canLogIn(status: Status): boolean {
  return status === Status.Active;
}

console.log(canLogIn(Status.Active)); // true

// Hata: Argument of type '"ACTIVE"' is not assignable to parameter of type 'Status'.
canLogIn("ACTIVE");

Son satır şaşırtıcı gelebilir: Status.Active'in değeri tam olarak "ACTIVE", ama string literal'ı kabul edilmiyor. String enum'lar TypeScript'in structural typing kuralının dışına çıkar ve neredeyse nominal davranır; değeri yalnızca enum'un kendisi üzerinden verebilirsin. Bazen istenen bir katılık bu, ama bir API'den gelen "ACTIVE" string'ini her seferinde dönüştürmen gerektiği anlamına da geliyor.

const enum: Kod Üretmeyen Enum

const enum, enum object'ini hiç üretmez; kullandığın her yere değerin kendisini gömer:

const enum Level {
  Low = 1,
  High = 2,
}

console.log(Level.High);

tsc'nin çıktısı tek satır: console.log(2 /* Level.High */);. Kulağa ideal geliyor, ama bunun için derleyicinin Level'ın tanımını görmesi gerekir. Level başka bir dosyadaysa, dosyaları tek tek dönüştüren araçlar (Node, çoğu bundler) değeri bilemez. TypeScript de bunu bildiği için isolatedModules ya da verbatimModuleSyntax açıkken export edilen bir const enum'ı gömmez, sıradan bir enum gibi derler. Yani modern bir tsconfig'de const enum'un tek avantajı da büyük ölçüde ortadan kalkıyor.

Namespace

Namespace, ES module'lerden önceki dönemden kalma bir gruplama aracı:

namespace Geometry {
  export const PI = 3.14;

  export function circleArea(radius: number): number {
    return PI * radius * radius;
  }
}

console.log(Geometry.circleArea(2)); // 12.56

Derlenmiş hâli yine bir fonksiyon ve bir object:

var Geometry;
(function (Geometry) {
  Geometry.PI = 3.14;
  function circleArea(radius) {
    return Geometry.PI * radius * radius;
  }
  Geometry.circleArea = circleArea;
})(Geometry || (Geometry = {}));

Bugün aynı işi bir module görür: geometry.ts dosyasında PI ve circleArea'yı export edersin, kullanan taraf import * as Geometry from "./geometry.ts" yazar. Bir istisna var: içinde yalnızca tip olan bir namespace çalışma zamanına hiçbir şey bırakmaz ve erasable sayılır. Namespace'in asıl yaşadığı yer ise declaration dosyaları; bir sonraki yazıda orada tekrar karşılaşacağız. Bir not daha: aynı şeyi yapan eski module Geometry { } yazımı TS 6.0'da kullanımdan kaldırıldı ve 7.0'da artık hata; anahtar kelime namespace.

Parameter Properties

Class'lar yazısında kısaca değindiğimiz parameter properties, constructor parametresine bir erişim belirteci koyarak property tanımlamayı ve atamayı tek seferde yapar:

class Account {
  constructor(
    public readonly id: number,
    private owner: string,
  ) {}
}

Kısa görünüyor, ama derleyici senin yazmadığın kodu üretiyor:

class Account {
  id;
  owner;
  constructor(id, owner) {
    this.id = id;
    this.owner = owner;
  }
}

Erasable karşılığı, aynı şeyi açıkça yazmak:

class Account {
  readonly id: number;
  private owner: string;

  constructor(id: number, owner: string) {
    this.id = id;
    this.owner = owner;
  }
}

Burada readonly ve private yalnızca tip bilgisi; silindiklerinde geriye geçerli bir class kalıyor. Sorun bu belirteçlerde değil, onların constructor parametresine konup kod üretmesinde.

erasableSyntaxOnly

TS 5.8 ile gelen erasableSyntaxOnly bayrağı, bu syntax'ların hepsini tip denetimi sırasında yakalar. Node için önerilen tsconfig'de bu yüzden açık:

// Hata: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
enum Direction {
  Up,
  Down,
}

// Hata: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
namespace Geometry {
  export const PI = 3.14;
}

class Account {
  // Hata: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
  constructor(public readonly id: number) {}
}

// Sorun yok: yalnızca tip içeren namespace silinebilir
namespace Shapes {
  export interface Point {
    x: number;
    y: number;
  }
}

Üç hata da aynı kodla gelir: TS1294. const enum ve import = da aynı mesajı alır. Böylece Node'un çalışma zamanında vereceği hatayı daha editördeyken görürsün.

Alternatif: as const Object ve Union

Enum'un yaptığı iki işi, yani bir değer kümesine isim vermeyi ve o kümeyi bir type olarak kullanmayı, tamamen erasable araçlarla yapabilirsin. Temel seride gördüğümüz as const ve bu seride gördüğümüz keyof typeof birleşince:

const Status = {
  Active: "ACTIVE",
  Banned: "BANNED",
} as const;

type Status = (typeof Status)[keyof typeof Status];

function describe(status: Status): string {
  switch (status) {
    case Status.Active:
      return "can log in";
    case "BANNED":
      return "blocked";
  }
}

console.log(describe("ACTIVE")); // can log in
console.log(describe(Status.Banned)); // blocked
console.log(Object.values(Status)); // [ 'ACTIVE', 'BANNED' ]

Status adı burada iki ayrı şey: bir değer (object) ve bir type ("ACTIVE" | "BANNED" union'ı). TypeScript'te değerler ve tipler ayrı isim alanlarında yaşadığı için bu çakışma değil, bilinçli bir eşleşme. Kazandıkların: Status.Active yazarak isimle de, "ACTIVE" yazarak doğrudan değerle de çalışabilirsin; Object.values yalnızca gerçek değerleri verir; derlenen kod yazdığın object'in ta kendisi. Tek bir yere isim vermen gerekmiyorsa düz bir union (type Direction = "up" | "down" | "left" | "right") daha da basit.

Sık Düşülen Tuzaklar

Numeric enum üzerinde Object.keys ya da Object.values ile dolaşmak. Reverse mapping yüzünden her üyeyi iki kez görürsün; baştaki örnekteki gibi.

Numeric enum'un tip güvenliğine fazla güvenmek. Literal sayılar denetleniyor, ama number tipindeki her değer kabul ediliyor. Dışarıdan gelen bir sayıyı enum'a atamadan önce kendin doğrulaman gerekir.

String enum'a string literal vermeye çalışmak. API'den ya da formdan gelen değerler düz string'dir; bunları enum'a çevirmek için ayrıca bir eşleme kodu yazman gerekir.

Başka bir dosyadaki const enum'ın her zaman gömüleceğini sanmak. Dosya dosya çalışan araçlarda ve isolatedModules açıkken bu garanti yok.

Enum'lu bir dosyayı node file.ts ile çalıştırıp hatayı ancak çalışma zamanında görmek. tsconfig'e erasableSyntaxOnly eklemek bu sürprizi derleme zamanına taşır.

Küçük Bir Deneme

★☆☆ Playground'da Direction enum'unu yaz, sağdaki ".JS" sekmesinde çıktıyı incele. Sonra enum'u const enum yap ve console.log(Direction.Left) satırının çıktısının nasıl değiştiğine bak.

★★☆ Tahmin et, sonra dene: enum Color { Red = "RED", Green = "GREEN" } için Object.keys(Color).length kaç olur? Numeric enum'dan farkını reverse mapping ile açıkla.

★★☆ Derlenir mi? erasableSyntaxOnly açıkken class Point { constructor(private x: number) {} } ve class Point { private x: number; constructor(x: number) { this.x = x; } }. Hangisi neden geçer?

★★★ Aşağıdaki enum'u as const object ve union ile yeniden yaz; function label(role: Role): string fonksiyonu her iki versiyonda da aynı şekilde çağrılabilsin: enum Role { Admin = "admin", Editor = "editor", Viewer = "viewer" }. Hangi çağrı biçimi yalnızca yeni versiyonda derleniyor?

Bu yazıda tipleri silindiğinde iz bırakan syntax'ları gördük. Bir sonraki yazıda bunun tam tersine bakacağız: hiç çalışma zamanı kodu içermeyen, yalnızca tiplerden oluşan dosyalara. Declaration dosyaları, yani .d.ts'ler, JavaScript ile yazılmış bir library'yi TypeScript'e nasıl tanıttığımızı ve declare anahtar kelimesinin ne işe yaradığını gösterecek; yalnızca tip içeren namespace'le de orada yeniden karşılaşacağız.