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

TypeScript ile Class'lar: Erişim Belirleyiciler, abstract, implements ve override

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript ile Class'lar: Erişim Belirleyiciler, abstract, implements ve override

TypeScript'in class'lara kattıklarını görüyoruz: property tipleri ve başlatma, public/private/protected ile #private farkı, readonly, abstract, implements, override ve parameter properties notu.

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

Önce Tahmin Et

Bir class'ta iki ayrı "gizli" property var. Biri TypeScript'in private anahtar kelimesiyle, diğeri JavaScript'in # işaretiyle tanımlanmış:

class Wallet {
  private balance = 100;
  #pin = 1234;
}

const wallet = new Wallet();
console.log(wallet.balance);
console.log(wallet["balance"]);
console.log(Object.keys(wallet));

İki soru: Hangi satır derlenmez? Derlenmeyen satırı silip kodu çalıştırırsan konsola ne yazar? İpucu: ikisi aynı şey değil. Cevap, iki private'ı yan yana koyduğumuz bölümde.

Property'leri Tanımlamak ve Başlatmak

JavaScript serilerinde class'ları, constructor'ı, class field'larını ve extends ile inheritance'ı görmüştük. Bu yazıda bunları tekrar anlatmayacağız; TypeScript'in class'lara eklediklerine bakacağız. İlki, property'lerin tiplerini class gövdesinde bildirmek:

class User {
  name: string;
  role = "member";
  // Hata: Property 'email' has no initializer and is not definitely assigned in the constructor.
  email: string;

  constructor(name: string) {
    this.name = name;
  }
}

name'in tipini açıkça yazdık, role'ün tipi ise başlangıç değerinden string olarak çıkarıldı. email ise hata veriyor. strict ailesindeki strictPropertyInitialization ayarı, tipi string olan bir property'nin constructor bittiğinde gerçekten bir string taşımasını şart koşar. email'e ne başlangıç değeri verdik ne de constructor'da atadık, yani instance oluştuğunda değeri undefined olurdu.

Bunu düzeltmenin üç dürüst yolu var: bir başlangıç değeri vermek, constructor'da atamak ya da property'yi email?: string diye optional yapmak. Dördüncü bir yol daha var: email!: string. Daha önce gördüğümüz non-null assertion gibi bu da derleyiciye "bana güven, ben atayacağım" der. Değeri gerçekten başka bir yerde (örneğin bir init() method'unda) atadığından emin değilsen kullanma; yanılırsan hatayı yine çalışma zamanında yakalarsın.

public, private ve protected

TypeScript, class üyelerinin nereden erişilebileceğini üç erişim belirleyicisiyle (access modifier) kontrol etmeni sağlar. Hiçbir şey yazmazsan üye public'tir, yani her yerden erişilebilir.

class Logger {
  protected prefix = "[app]";

  public info(message: string): void {
    console.log(`${this.prefix} ${message}`);
  }
}

class RequestLogger extends Logger {
  request(path: string): void {
    this.info(`GET ${path}`);
    console.log(this.prefix.length);
  }
}

const logger = new RequestLogger();
logger.request("/users");
// Hata: Property 'prefix' is protected and only accessible within class 'Logger' and its subclasses.
console.log(logger.prefix);

private bir üyeye sadece tanımlandığı class'ın içinden erişilebilir. protected bir üye ise hem o class'tan hem de ondan türeyen class'lardan erişilebilir, ama dışarıdan erişilemez. RequestLogger içinde this.prefix kullanmak serbest; logger.prefix ise class'ın dışında kaldığı için hata veriyor. public'i açıkça yazmak zorunlu değil; çoğu kod tabanında yazılmaz, sadece okunabilirlik için tercih eden ekipler vardır.

private mı, #private mı?

Şimdi baştaki soruya dönelim. wallet.balance satırı derlenmez:

error TS2341: Property 'balance' is private and only accessible within class 'Wallet'.

O satırı silip çalıştırdığında konsolda şunu görürsün:

100
[ 'balance' ]

İşte kritik fark. TypeScript'in private'ı sadece derleme zamanında var olan bir kuraldır. Tipler silindiğinde private kelimesi de silinir ve geriye sıradan bir property kalır. wallet["balance"] gibi köşeli parantezli erişime TypeScript bilerek izin verir (bir kaçış kapısı olarak), Object.keys property'yi listeler, JSON.stringify onu çıktıya yazar. #pin ise JavaScript'in kendi private field'ıdır; çalışma zamanında da gerçekten gizlidir, Object.keys'te görünmez ve class dışından ona ulaşmanın bir yolu yoktur. TypeScript de bunu derleme zamanında yakalar: wallet.#pin yazarsan Property '#pin' is not accessible outside class 'Wallet' because it has a private identifier. hatasını alırsın.

Hangisini kullanmalı? Gerçek gizlilik istiyorsan, yani değerin dışarıdan hiçbir şekilde okunmaması gerekiyorsa # kullan. private ise "bu class'ın iç detayı, dokunma" diyen bir anlaşmadır; test kodunda ya da hata ayıklarken içine bakabilmek bazen işe yarar. Bir de şunu unutma: protected'ın JavaScript'te bir karşılığı yoktur. Türeyen class'larla paylaşılacak bir üye için TypeScript'in belirleyicileri tek seçenektir.

readonly

Daha önce object tiplerinde gördüğümüz readonly, class property'lerinde de çalışır:

class Order {
  readonly id: string;
  readonly createdAt = new Date();

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

const order = new Order("A-100");
// Hata: Cannot assign to 'id' because it is a read-only property.
order.id = "A-200";

Bir readonly property'ye sadece tanımlandığı yerde ya da constructor'ın içinde değer atanabilir. Sonrasında, class'ın kendi method'larından bile değiştirilemez. Ama bu koruma da sadece derleme zamanındadır ve yüzeyseldir: readonly tags: string[] property'sine yeni bir array atayamazsın, ama order.tags.push("gift") çalışır. İçeriği de korumak istiyorsan tipi readonly string[] yaz.

abstract Class'lar

Bazen bir class'ın kendi başına instance'ı olmasını istemezsin; sadece ortak davranışı tanımlayan bir temel olsun, eksik parçaları türeyen class'lar tamamlasın. abstract bunun içindir:

abstract class Shape {
  abstract area(): number;

  describe(): string {
    return `Area: ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  #radius: number;

  constructor(radius: number) {
    super();
    this.#radius = radius;
  }

  area(): number {
    return Math.PI * this.#radius ** 2;
  }
}

console.log(new Circle(2).describe()); // Area: 12.57
// Hata: Cannot create an instance of an abstract class.
const shape = new Shape();

abstract area(): number bir gövde içermez; sadece "benden türeyen her class bu method'u yazmak zorunda" der. describe() ise sıradan bir method ve henüz yazılmamış area()'yı güvenle çağırabiliyor. Circle, area()'yı yazmayı unutsaydı Non-abstract class 'Circle' does not implement inherited abstract member area from class 'Shape'. hatası alırdın. new Shape() ise doğrudan reddediliyor.

implements: Bir Class'ı Bir Interface'e Bağlamak

Inheritance bir class'tan davranış devralır. implements ise hiçbir şey devralmaz; sadece class'ın bir interface'in tarif ettiği yapıya uyduğunu denetler:

interface Greeter {
  greet(name: string): string;
}

interface Named {
  readonly label: string;
}

class FriendlyGreeter implements Greeter, Named {
  readonly label = "friendly";

  greet(name: string): string {
    return `Hi, ${name}!`;
  }
}

console.log(new FriendlyGreeter().greet("Ada")); // Hi, Ada!

Bir class birden fazla interface'i implement edebilir, ama yalnızca tek bir class'tan türeyebilir. greet'i yazmayı unutursan Class 'FriendlyGreeter' incorrectly implements interface 'Greeter'. hatasını alırsın. Bir şeyi bilmekte fayda var: implements sadece bir kontroldür, class'ın tipini değiştirmez. Daha önce gördüğümüz structural typing yüzünden implements yazmasan da aynı yapıdaki bir class Greeter beklenen yere verilebilir. implements'in değeri, uyumsuzluğu class'ı yazarken, class'ın kendi satırında yakalamasıdır.

override ve noImplicitOverride

Türeyen bir class, üst class'taki bir method'u yeniden yazabilir. Peki method adını yanlış yazarsan ne olur? JavaScript yeni bir method tanımladığını düşünür ve sessizce devam eder. TypeScript'in override anahtar kelimesi bunu yakalar:

class Animal {
  speak(): string {
    return "...";
  }
}

class Dog extends Animal {
  override speak(): string {
    return "Woof";
  }
}

class Cat extends Animal {
  // Hata: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?
  override speek(): string {
    return "Meow";
  }
}

override derleyiciye "bu method üst class'ta var olan bir method'u eziyor" der ve bu doğru değilse hata verir. Ama override yazmak isteğe bağlı olduğu sürece, unutulduğunda hiçbir koruma sağlamaz. Bunun için noImplicitOverride ayarı vardır: açık olduğunda, üst class'taki bir üyeyi ezen her üyeye override yazmak zorunlu hâle gelir. Yukarıdaki Dog'da override'ı silersen This member must have an 'override' modifier because it overrides a member in the base class 'Animal'. hatası gelir. Bu ayar strict ailesine dahil değildir ve tsc --init'in ürettiği tsconfig'de de yorum satırı olarak durur. Inheritance kullanan projelerde açmanı öneririm.

Sık Düşülen Tuzaklar

Birinci tuzak: implements'in parametre tiplerini class'a aktaracağını sanmak. Aktarmaz. Interface'te greet(name: string) yazıyor olsa bile class'ta greet(name) yazarsan Parameter 'name' implicitly has an 'any' type. hatasını alırsın. Class'taki method'un tiplerini kendin yazman gerekir.

İkinci tuzak: private'ı bir güvenlik önlemi sanmak. Önce Tahmin Et bölümünde gördüğün gibi çalışma zamanında hiçbir şeyi gizlemez. Bir şifreyi, token'ı ya da gerçekten dışarı sızmaması gereken bir değeri tutuyorsan # kullan.

Üçüncü tuzak, başka kod tabanlarında sık göreceğin bir kısayol: parameter properties.

class Point {
  // Hata (erasableSyntaxOnly açıkken): This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
  constructor(private x: number, private y: number) {}
}

Constructor parametresinin önüne private, public ya da readonly yazmak, o parametreyi otomatik olarak bir property'ye dönüştürür. Kısa görünür, ama bu syntax tipler silinince ortadan kalkmaz; derleyicinin constructor'a this.x = x gibi gerçek JavaScript kodu eklemesi gerekir. Bu yüzden "erasable" değildir: Node'un type stripping'i bu kodu ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX hatasıyla reddeder, erasableSyntaxOnly açık bir projede ise tsc yukarıdaki hatayı verir. Bu serideki örneklerde property'leri her zaman açıkça tanımlıyoruz. TypeScript'e özgü bu tür syntax'ları enum ve namespace ile birlikte ilerleyen yazılarda ayrıca konuşacağız.

Küçük Bir Deneme

★☆☆ Wallet örneğine console.log(JSON.stringify(wallet)); satırını ekle. Çıktıda balance ve #pin'den hangilerinin görüneceğini tahmin et, sonra çalıştırıp kontrol et.

★★☆ Derlenir mi, tahmin et: Wallet class'ına isRicherThan(other: Wallet): boolean { return this.balance > other.balance; } method'unu ekle. other.balance başka bir instance'ın private property'si; TypeScript'in private'ı instance'a mı, class'a mı bağlı?

★★★ Shape'ten türeyen bir Rectangle class'ı yaz; #width ve #height field'ları olsun ve area()'yı implement etsin. Sonra komut satırında npx tsc --noEmit --noImplicitOverride ile denetle: area() için override yazman gerekiyor mu? Abstract bir method'u implement etmekle var olan bir method'u ezmek arasındaki farkı gözlemle.

Bu yazıda class'ların yapısını tiplerle tarif ettik. Bir sonraki yazıda tersini yapacağız: var olan değerlerden ve object'lerden tip türeteceğiz. Tip seviyesindeki typeof ile bir değerin tipini yakalamayı, keyof ile bir tipin key'lerinden union üretmeyi ve ikisini keyof typeof ile birlikte kullanmayı göreceğiz.