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üyelerinT'ye erişememesi - Sınıfın hem tip hem değer üretmesi;
typeof Sinifve 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? | Evet | Hayır |
| JSON.stringify çıktısında | Gö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 tercih | Uygulama kodunda yeterli ve sade; Angular/NestJS geleneği | Gerç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 Deposuyazı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
implementsaslı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. derstekiextendsvs&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) | |
|---|---|---|
| Durum | Dilin resmî yolu; varsayılan | Eski öneri; bayrakla açılır |
| Kim kullanıyor | Yeni kütüphaneler, saf TS projeleri | Angular ve NestJS (tüm ekosistemleriyle) |
| Parametre dekoratörü | Yok | Var — DI'ın (@Inject) dayandığı özellik |
| Metadata | Kendi 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
- Başlatma disiplini: atanmamış
ad: stringalanıylaTS2564'ü gör; üç yolla çöz (bildirimde değer, kurucuda atama,| undefined). Sonra!ile sustur ve bunun hangi riski aldığını yaz. - İki gizlilik:
private bakiyeve#pinalanlı sınıf kur.(h as any).bakiyeve(h as any)["#pin"]dene; Playground'un .JS sekmesindeprivate'ın izini ara — bulabildin mi? - Parametre özelliği:
constructor(private readonly http: X)yazıp .JS çıktısında üretilen atamayı gör. TS Config'tenerasableSyntaxOnly'yi açıp hatayı oku; klasik yazıma çevir. - implements izi: bir arayüzü
implementseden 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. - instanceof tuzağı:
x instanceof Deposuyazmayı dene (arayüzle). Hata mesajı ne diyor? Aynı ayrımı sınıfla yapınca neden çalışıyor? - Soyut sınıf:
TabanDepo'yu kur; doğrudan örneklemeyi ve soyut üyeyi doldurmadan kalıtmayı dene — iki ayrı hatayı da gör. - Generic depo:
Depo<T extends { id: number }>'yu yaz;id'siz tiple örneklemeyi dene, sonrastatic son: TekleyipTS2302'yi oku. - Ç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? - Nominalleşme: aynı şekilli iki sınıfı önce yalnız
publicalanlarla, sonra birerprivatealan ekleyerek birbirine ata. Davranış nasıl değişti? Bunun 9. dersteki markalı tiple ilişkisini bir cümleyle kur. - İlk dekoratörün:
loglusı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."
