Skip to content

Bölüm 7 / 10

Sınıflar ve dekoratörler

Angular ve NestJS'in temeli.

13 dakikalık okuma

Sınıfları 1. hatta kavram, 3. hatta C# derinliğiyle, JavaScript sözdizimini de 8. hatta gördün. TypeScript sınıfa iki şey ekler: C#'tan tanıdığın erişim belirleyiciler ve arayüz sözleşmeleri — ama derleme anında yaşayan, silinen türden — ve tip sisteminin sınıfa özgü incelikleri. Sonda, Angular'ın ve NestJS'in üzerine kurulduğu dekoratör mekanizması var: bu ders kavramı verir, gerçek kullanımını 10. ve 12. hatlarda göreceksin.

01Ne öğreneceksin

  • Sınıf alanlarının tiplenmesi ve strictPropertyInitialization — kesin atama ! ne zaman meşru
  • public/private/protected'in derleme anında silinmesi; # ile farkı ve karar
  • Parametre özellikleri: Angular/NestJS kurucularının kısayolu — ve silinme kuralıyla çatışması
  • implements'in yalnız denetim olduğu; soyut sınıf ile arayüz kararı
  • Generic sınıf: Depo<T>; static üyelerin T'ye erişememesi
  • Sınıfın hem tip hem değer üretmesi; typeof Sinif ve kurucu tipleri
  • Yapısal sistemde sınıflar; private üyenin tipi nominalleştirmesi
  • Dekoratörler: ne oldukları, standart ile deneysel sözdiziminin farkı, Angular/NestJS köprüsü

02Alanlar ve başlatma disiplini

class BasvuruServisi {
  taban = "/api";                    // çıkarım: string
  sayac: number = 0;                 // açık tip — değerden belliyse gereksiz
  son: Basvuru | null = null;        // null ile başlarsa tipte söylenir
  ad: string;                        // TS2564 — hiçbir yerde atanmıyor!

  constructor(ad: string) {
    this.ad = ad;                    // kurucuda atanırsa TS2564 kalkar
  }
}

2. dersteki tabloda geçen strictPropertyInitialization burada devreye girer: her alan ya bildirimde ya kurucuda kesin olarak değer almalıdır. Bu, "alan tanımlandı ama atanması unutuldu, ilk erişimde undefined patladı" hatasını derleme anına çeker. Derleyicinin göremediği meşru durumlar için kesin atama işareti vardır:

class Bilesen {
  eleman!: HTMLElement;    // "bana güven, kullanmadan önce atanmış olacak"
  baslat() { this.eleman = document.querySelector("#kok")!; }
}

! işareti bir kontrol değil sözdür — as ailesinin akrabası. Meşru olduğu yerler dardır: yaşam döngüsü gereği kurucu dışında başlatılan alanlar (test kurulumları, çerçeve kancaları). Varsayılan refleksin o değil, kurucuda atamak ya da tipe | null ekleyip dürüst olmak olmalı.

03Erişim belirleyiciler: iki ayrı gizlilik

class Hesap {
  private bakiye = 0;            // TypeScript gizliliği — DERLEMEDE silinir
  #pin = "1234";                 // JavaScript gizliliği — ÇALIŞMA ANINDA gerçek
  protected tur = "vadesiz";     // alt sınıflar erişir, dışarısı erişemez
  readonly iban: string;         // kurucudan sonra atanamaz (3. dersin kuralı)

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

const h = new Hesap("TR33…");
h.bakiye;                        // TS2341 — derleyici engeller…
(h as any).bakiye;               // …ama çalışma anında ALAN ORADADIR
(h as any).#pin;                 // sözdizimi bile yok; gerçekten erişilemez
private (TS)#alan (JS)
Denetim zamanıDerleme — çıktıda iz yokÇalışma anı — motor engeller
as any ile delinir mi?EvetHayır
JSON.stringify çıktısındaGörünür (sıradan alan)Görünmez
Tip uyumluluğuna etkisiİkisi de sınıfı nominalleştirir (7. bölüm)
Yaygın tercihUygulama kodunda yeterli ve sade; Angular/NestJS geleneğiGerçek gizlilik gerekince (kütüphane, güvenlik sınırı)

Karar çoğu ekipte pragmatiktir: sınıf içi disiplin için private yeter — kısa, okunaklı ve ekosistemin (Angular, NestJS) alışkanlığı budur. Alanın gerçekten dışarıdan okunamaması bir gereklilikse (yeteneğin kendisi güvenlik sınırıysa) # kullanılır; 8. hatta gördüğün gibi bu, dilin kendi mekanizmasıdır ve tip silinmesinden etkilenmez.

04Parametre özellikleri: kurucu kısayolu

Şu kalıbı Angular ve NestJS kodunda binlerce kez göreceksin:

class BasvuruServisi {
  constructor(
    private readonly http: HttpIstemci,
    private readonly gunluk: Gunlukcu,
  ) {}
  // "private readonly http" HEM parametre HEM alan bildirir HEM atar —
  // this.http = http satırı yazılmaz.
}

Buna parametre özelliği (parameter property) denir: kurucu parametresine erişim belirleyici (ya da readonly) yazıldığında TypeScript aynı adla bir alan üretip atamayı kendisi yapar. Bağımlılık enjeksiyonu (5. hatta gördüğün DI) kurucularının standart yazımıdır — Angular/NestJS servislerinin neredeyse tamamı bu biçimdedir.

Dürüst dipnot: bu özellik, enum gibi (2. ders), silinme kuralını bozan sözdizimindendir — derleyici çalışma anına atama kodu üretir. Bu yüzden erasableSyntaxOnly bayrağı açık projelerde parametre özellikleri de hatadır ve klasik this.http = http yazımına dönülür. Angular/NestJS kendi derleme zincirini kurduğu için orada sorun değildir; tipleri soyup çalıştıran ortamları (1. ders) hedefleyen kodda ise kaçınılır. İkisini de okuyabilmen gerekir.

05implements ve soyut sınıf

interface Deposu {
  ekle(b: Basvuru): void;
  bul(id: number): Basvuru | undefined;
}

class BellekDeposu implements Deposu {
  private kayitlar = new Map<number, Basvuru>();
  ekle(b: Basvuru) { this.kayitlar.set(b.id, b); }
  bul(id: number)  { return this.kayitlar.get(id); }
}

implements yalnız bir denetimdir: sınıfın, arayüzün istediği üyeleri taşıdığını doğrular; çalışma anına hiçbir iz bırakmaz. Buradan iki pratik sonuç çıkar:

  • x instanceof Deposu yazılamaz — arayüz çalışma anında yoktur (4. dersteki daraltma bu yüzden sınıflarla ya da tip koruyucularla yapılır).
  • Yapısal sistemde implements aslında zorunlu bile değildir — şekli tutan her sınıf arayüze uyar. Yine de yazılır, çünkü hatayı sınıfın bildirildiği yerde patlatır; yazmazsan hata, sınıfı arayüz bekleyen yere verdiğin uzak noktada çıkar. 3. dersteki extends vs & gerekçesinin aynısı: hatayı sebebine yaklaştır.

Soyut sınıf ise çalışma anında vardır: ortak davranış + boş bırakılmış sözleşme karışımı (1. hattaki kavram, 3. hattaki C# karşılığıyla aynı):

abstract class TabanDepo implements Deposu {
  abstract ekle(b: Basvuru): void;             // alt sınıf DOLDURMAK zorunda
  abstract bul(id: number): Basvuru | undefined;
  say(): number { return this.hepsi().length; } // ORTAK davranış paylaşılıyor
  protected abstract hepsi(): Basvuru[];
}
new TabanDepo();                               // TS2511 — soyut sınıf örneklenemez

Karar 4. hattaki ilkelerle aynıdır: yalnız sözleşme gerekiyorsa arayüz (hafif, çoklu uygulanır, çalışma anı maliyeti sıfır); paylaşılan davranış + sözleşme gerekiyorsa soyut sınıf. Kompozisyonun kalıtımdan önce düşünüldüğü kural da aynen taşınır.

06Generic sınıf ve static sınırı

class Depo<T extends { id: number }> {          // 5. dersin kısıtı iş başında
  private kayitlar = new Map<number, T>();

  ekle(kayit: T): void { this.kayitlar.set(kayit.id, kayit); }
  bul(id: number): T | undefined { return this.kayitlar.get(id); }
  hepsi(): readonly T[] { return [...this.kayitlar.values()]; }

  static surum = "1.0";              // olur
  // static son: T;                  // TS2302 — static, T'yi GÖREMEZ
}

const basvurular = new Depo<Basvuru>();
const b = basvurular.bul(5);         // tipi: Basvuru | undefined — dürüst

static üyeler T'ye erişemez; mantıklıdır, çünkü T örnek başına bağlanır (Depo<Basvuru>, Depo<Kullanici>) ama static alan sınıfın kendisinde tektir — hangi T'yi tutacaktı? Bir de sınıfın çift kimliğini netleştirelim: class bildirimi iki şey üretir — örneklerin tipi (Depo<Basvuru>) ve kurucu değeri. Kurucunun tipi typeof Depo'dur; "bana bir sınıf ver, ben örnekleyeyim" diyen fabrika fonksiyonları kurucu imzasıyla yazılır:

function uret<T>(Sinif: new () => T): T { return new Sinif(); }

Bu ayrım Angular'da her yerdedir: @Injectable servis kaydı sınıf değerini alır, değişken tipleri örnek tipini kullanır.

07Sınıflar yapısaldır — private nominalleştirir

class Nokta   { constructor(public x: number, public y: number) {} }
class Vektor  { constructor(public x: number, public y: number) {} }
const n: Nokta = new Vektor(1, 2);   // OLUR — 3. dersin yapısal kuralı sınıfta da geçerli

class HesapA { private bakiye = 0 }
class HesapB { private bakiye = 0 }
const a: HesapA = new HesapB();      // TS2322 — private üye MARKADIR:
// "aynı adlı private alan" bile olsa iki sınıf artık değiştirilemez

Kural: private/protected (ya da #) üyesi olan sınıflar, o üye aynı bildirimden gelmedikçe birbirine atanamaz. Yapısal sistemin içinde küçük bir nominal ada: gizli üye, sınıfın kimliğini tipe mühürler. Bunun pratik yararı, 3. derste sözü geçen "markalı tip" ihtiyacının sınıflarda kendiliğinden çözülmesidir — KullaniciId ve BasvuruId ayrı sınıflarsa ve birer private üye taşıyorlarsa karışmaları imkânsızdır. Nesne tipleri için aynı tekniği 9. derste markalı tiplerle kuracağız.

08Dekoratörler: sınıfa iliştirilen davranış

Dekoratör, bir sınıfın (ya da üyesinin) tanımını alıp değiştiren veya kaydeden fonksiyondur; 4. hatta gördüğün Decorator tasarım desenin dile gömülmüş hâli gibidir ve @ad sözdizimiyle iliştirilir:

function loglu<T extends new (...args: any[]) => object>(Sinif: T) {
  return class extends Sinif {
    constructor(...args: any[]) {
      super(...args);
      console.log(`${Sinif.name} örneklendi`);
    }
  };
}

@loglu
class BasvuruServisi { … }
new BasvuruServisi();        // konsol: "BasvuruServisi örneklendi"

Bilmen gereken kritik gerçek, iki ayrı dekoratör dünyası olduğudur:

Standart (TC39, TS 5.0+)Deneysel (experimentalDecorators)
DurumDilin resmî yolu; varsayılanEski öneri; bayrakla açılır
Kim kullanıyorYeni kütüphaneler, saf TS projeleriAngular ve NestJS (tüm ekosistemleriyle)
Parametre dekoratörüYokVar — DI'ın (@Inject) dayandığı özellik
MetadataKendi mekanizmasıemitDecoratorMetadata + reflect ile tip bilgisini çalışma anına taşır

Angular'ın @Component({ selector: … }), @Injectable() ve NestJS'in @Controller("basvuru"), @Get(":id") yazımları hep dekoratördür: sınıfı çerçevenin kayıt defterine üst veriyle birlikte eklerler — "bu sınıf bir bileşendir, şablonu şudur", "bu metot GET isteğini karşılar". Deneysel dünyada emitDecoratorMetadata, kurucu parametrelerinin tiplerini çalışma anına taşır; NestJS'in "tipten anlayıp doğru servisi enjekte etmesi" bu istisnai mekanizmayla çalışır — tip silinmesi kuralının bilinçli olarak delindiği nadir yerdir. Bu derste bu kadarı yeter: dekoratör görünce "sınıfı işleyen fonksiyon + kayıt/üst veri" diye oku; yazma pratiğini 10. hatta Angular'la, 12. hatta NestJS'le yapacaksın.

09Mini alıştırma — Sınıfı derleyiciyle sınadır

Yaklaşık 20 dakika · TypeScript Playground, strict açık

  1. Başlatma disiplini: atanmamış ad: string alanıyla TS2564'ü gör; üç yolla çöz (bildirimde değer, kurucuda atama, | undefined). Sonra ! ile sustur ve bunun hangi riski aldığını yaz.
  2. İki gizlilik: private bakiye ve #pin alanlı sınıf kur. (h as any).bakiye ve (h as any)["#pin"] dene; Playground'un .JS sekmesinde private'ın izini ara — bulabildin mi?
  3. Parametre özelliği: constructor(private readonly http: X) yazıp .JS çıktısında üretilen atamayı gör. TS Config'ten erasableSyntaxOnly'yi açıp hatayı oku; klasik yazıma çevir.
  4. implements izi: bir arayüzü implements eden sınıftan bir metodu sil — hata nerede çıktı? implements'i silip aynı sınıfı arayüz bekleyen fonksiyona ver — hata şimdi nerede? İki konumu karşılaştır.
  5. instanceof tuzağı: x instanceof Deposu yazmayı dene (arayüzle). Hata mesajı ne diyor? Aynı ayrımı sınıfla yapınca neden çalışıyor?
  6. Soyut sınıf: TabanDepo'yu kur; doğrudan örneklemeyi ve soyut üyeyi doldurmadan kalıtmayı dene — iki ayrı hatayı da gör.
  7. Generic depo: Depo<T extends { id: number }>'yu yaz; id'siz tiple örneklemeyi dene, sonra static son: T ekleyip TS2302'yi oku.
  8. Çift kimlik: uret(Sinif: new () => T) fabrikasını yaz; parametresiz kuruculu bir sınıfla çağır, parametreli olanla da dene. Hangi imza hangi hatayı verdi?
  9. Nominalleşme: aynı şekilli iki sınıfı önce yalnız public alanlarla, sonra birer private alan ekleyerek birbirine ata. Davranış nasıl değişti? Bunun 9. dersteki markalı tiple ilişkisini bir cümleyle kur.
  10. İlk dekoratörün: loglu sınıf dekoratörünü yaz ve örneklemede mesajın düştüğünü çalıştırarak gör. Sonra dekoratörü bir de metoda uygulamayı dene ve imza farkını gözle.

10Kendini sına

TypeScript'in private'ı ile JavaScript'in # alanı arasındaki fark nedir? Hangisini ne zaman seçersin?

İkisi ayrı katmanlarda çalışır. private bir derleme anı denetimidir: ihlali TS2341 ile durdurur ama tip silinmesiyle birlikte kaybolur — çıktıda alan sıradan bir özelliktir, as any ile erişilir, JSON.stringify çıktısında görünür. # ise dilin kendi mekanizmasıdır (8. hat): gizlilik çalışma anında motor tarafından uygulanır, dışarıdan erişimin sözdizimi bile yoktur ve serileştirmede görünmez. Ortak noktaları, ikisinin de tip uyumluluğunu nominalleştirmesidir: gizli üyesi olan iki sınıf, şekilleri aynı olsa da birbirine atanamaz. Seçim: uygulama içi disiplin — "bu alana sınıf dışından dokunulmasın" — için private yeterlidir; kısa ve okunaklıdır, parametre özellikleriyle bütünleşir ve Angular/NestJS geleneği budur. Gizliliğin kendisi bir gereklilikse — kütüphane sınırı, sızmaması gereken değer, serileştirme davranışı — # doğru araçtır çünkü güvencesi derleyiciye değil çalışma ortamına dayanır. İkisini karıştırmamak için ekip içinde tek kural seçilir.

implements ne yapar, ne yapmaz? Arayüz ile soyut sınıf arasında nasıl seçim yaparsın?

implements yalnız bir derleme anı denetimidir: sınıfın, arayüzün üyelerini doğru tiplerle taşıdığını doğrular ve çalışma anına hiçbir iz bırakmaz. Yapmadıkları: kalıtım kurmaz, davranış paylaşmaz ve — önemli tuzak — instanceof ile sınanamaz; arayüz çalışma anında var olmadığından daraltma sınıflarla ya da tip koruyucularla yapılır. Yapısal sistemde aslında zorunlu da değildir — şekli tutan sınıf zaten uyar — ama yazılır, çünkü eksik/yanlış üyeyi sınıfın bildirildiği satırda patlatır; yazılmazsa hata, sınıfın arayüz beklenen yere verildiği uzak noktada çıkar ve sebep-sonuç mesafesi açılır. Seçim ölçütü: yalnız sözleşme gerekiyorsa arayüz — hafiftir, bir sınıf birden çok arayüz uygular, test için sahtesi düz nesneyle yazılır. Sözleşme + paylaşılan davranış gerekiyorsa soyut sınıf — çalışma anında vardır, ortak metotlar bir kez yazılır, soyut üyeler alt sınıfı doldurmaya zorlar, doğrudan örneklenemez. Kalıtım zinciri derinleşiyorsa 4. hattın uyarısı geçerlidir: kompozisyonu önce düşün; soyut sınıf tek kalıtım hakkını harcar.

Parametre özelliği (parameter property) nedir? Hangi bayrakla neden çatışır?

Kurucu parametresine erişim belirleyici ya da readonly yazıldığında (constructor(private readonly http: HttpIstemci)) TypeScript'in aynı adla bir sınıf alanı bildirip atamayı kendiliğinden yapmasıdır — this.http = http satırı ve ayrı alan bildirimi yazılmaz. Bağımlılık enjeksiyonu kurucularının standart yazımıdır; Angular ve NestJS servislerinin neredeyse tamamı bu biçimdedir, bu yüzden okumayı bilmek zorunludur. Çatışma şuradadır: TypeScript'in genel kuralı "tipler silinir, davranış değişmez"dir (1. ders); parametre özelliği ise bu kuralı bozan az sayıdaki sözdiziminden biridir — derleyici çalışma anına gerçek atama kodu üretir. Tipleri yalnız soyarak çalıştıran araçlar (Node'un strip-types'ı) bu dönüşümü yapamayacağı için, silinebilir-sözdizimi disiplinini açan erasableSyntaxOnly bayrağı parametre özelliklerini (enum ve namespace ile birlikte) hataya çevirir. Angular/NestJS kendi derleme zincirine sahip olduğu için oralarda sorun yoktur; hedefin tip-soyucu ortamlarsa klasik yazıma dönersin. Yani karar proje zincirine bağlıdır — iki biçimi de okuyup yazabilmek gerekir.

Dekoratör nedir? Angular'daki @Component gördüğünde ne olduğunu nasıl açıklarsın?

Dekoratör, bir sınıf ya da üye tanımını parametre olarak alıp onu değiştiren, saran ya da bir yere kaydeden fonksiyondur; @ad sözdizimiyle tanımın üzerine iliştirilir — Decorator tasarım deseninin (4. hat) dile gömülmüş biçimi gibi düşünülebilir. İki dünya vardır: TC39'un standart dekoratörleri (TS 5.0+ varsayılanı) ve Angular ile NestJS'in kullandığı deneysel sözdizimi (experimentalDecorators); deneysel dünyada parametre dekoratörleri ve emitDecoratorMetadata vardır — kurucu parametrelerinin tip bilgisini çalışma anına taşır ki bu, tip silinmesi kuralının bilinçli delindiği istisnai noktadır ve NestJS'in "tipe bakıp doğru bağımlılığı enjekte etmesi" buna dayanır. @Component({ selector: "app-liste", templateUrl: … }) gördüğünde okuma şudur: "bu fonksiyon, sınıfı Angular'ın kayıt defterine şu üst veriyle ekliyor — sınıf artık sıradan bir sınıf değil, çerçevenin tanıdığı bir bileşen." @Injectable aynı işin servis hâli, NestJS'in @Controller/@Get'i rota kaydı hâlidir. Sınıfın kendisi yine sınıftır; dekoratör ona davranış ve kimlik iliştirir. Yazma pratiği 10. ve 12. hatların işidir.

11Sınav köşesi

Yazılıda nasıl sorulur

(1) TypeScript'te erişim belirleyiciler nelerdir; # ile private farkı nedir? (2) readonly sınıf üyesi ne sağlar? (3) Parameter property nedir? (4) implements ile extends farkı. (5) Abstract class ile interface farkı; hangisi ne zaman? (6) Neden instanceof bir arayüzle kullanılamaz? (7) strictPropertyInitialization ne denetler; ! işareti ne yapar? (8) Generic sınıf nasıl tanımlanır; static üye neden tip parametresine erişemez? (9) typeof Sinif neyi ifade eder; kurucu tipi (new () => T) nerede kullanılır? (10) Dekoratör nedir; Angular ve NestJS'te ne işe yarar? (11) experimentalDecorators ve emitDecoratorMetadata nedir? (12) Private üyeli sınıflar neden yapısal olarak uyumsuzdur?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Çerçeve — "TypeScript sınıfa C# konforunu getirir ama derleme anında: private/protected/readonly denetlenir ve silinir; gerçek gizlilik gerekiyorsa dilin # alanı kullanılır — ikisi de sınıfı nominalleştirir." (2) Disiplin — "alanlar kesin başlatılır (strictPropertyInitialization); ! kontrol değil sözdür, dar kullanırım. DI kurucularını parametre özellikleriyle yazarım — erasableSyntaxOnly ile çatıştığını bilirim." (3) Sözleşme — "implements yalnız denetimdir, çalışma anına iz bırakmaz — instanceof arayüzle çalışmaz; davranış paylaşımı gerekiyorsa soyut sınıf, yoksa arayüz + kompozisyon." (4) Kıdem vuruşu — "sınıf hem tip hem değer üretir: örnek tipi ile typeof Sinif ayrımını ve fabrika imzasını (new () => T) bilirim; generic depoda static, T'yi göremez." (5) Köprü — "Angular/NestJS dekoratör üzerine kuruludur: sınıfı üst veriyle çerçeveye kaydeden fonksiyon. Standart ve deneysel sözdizimi ayrımını, metadata'nın tip silinmesini deldiği istisnayı söylerim."