Skip to content

Bölüm 4 / 10

Union ve tip daraltma

Union, intersection, literal tipler, type guard.

13 dakikalık okuma

TypeScript'in günlük pratiği büyük ölçüde iki hareketten oluşur: bir değerin olabileceği hâlleri birleşimle ifade etmek, sonra kodun her dalında bu hâlleri daraltarak tek tek ele almak. Derleyici bunu yaparken senin if'lerini okur — buna kontrol akışı analizi denir ve TypeScript'i "tip yazdığın JavaScript"ten "seninle birlikte düşünen derleyici"ye çeviren şey budur. Bu ders 8. hattın iki senedini de öder: doğruluk değeri tuzakları ve catch içindeki hata sınıfları.

01Ne öğreneceksin

  • Birleşim (union) tiplerinde yalnız ortak üyelere erişilebilmesi kuralı
  • Literal tipler, genişletme ve as const'un daraltıcı gücü
  • Daraltma araçları: typeof, eşitlik, in, instanceof — ve typeof null tuzağının dönüşü
  • Doğruluk değeri daraltması ve 0/boş dize tuzağı; ?? ?. != null üçlüsünün tiple bütünleşmesi
  • catch (e: unknown) içinde hata daraltma — 8. hattın ApiHatasi senedi
  • Ayırt edici birleşim: durum modellemenin üretim kalıbı + never ile tüketilmişlik denetimi
  • Kullanıcı tanımlı tip koruyucular (x is T) ve yalan söyleme riskleri
  • Akış analizinin sınırları: geri çağrılar, mutasyon, fonksiyon sınırları

02Birleşim: "şunlardan biri"

type Kimlik = number | string;

function biletBul(id: Kimlik) {
  id.toFixed(2);        // TS2339 — ya string ise?
  id.toUpperCase();     // TS2339 — ya number ise?
  id.toString();        // OLUR — İKİSİNDE DE var
}

Kural nettir: birleşim tipli değer üzerinde yalnız tüm üyelerde ortak olan şeylere erişebilirsin. Gerisi için önce hangi üyede olduğunu kanıtlaman — daraltman — gerekir. Bu, JavaScript'te zaten yazdığın savunmacı kontrollerin ta kendisidir; fark, TypeScript'in bu kontrolleri okuyup tipe işlemesi ve unuttuğunda seni durdurmasıdır.

03Literal tipler: değerin kendisi tip olunca

2. derste görmüştün: "asc" yalnız bir değer değil, aynı zamanda bir tiptir — tek elemanlı bir tip. Literallerin birleşimi, kümesi sınırlı değerleri ifade etmenin standart yoludur:

type Yon = "asc" | "desc";
type ZarDegeri = 1 | 2 | 3 | 4 | 5 | 6;
type Cevap = true | "belki";              // karışık da olur

function sirala(alan: string, yon: Yon) { … }
sirala("ad", "desc");     // editör iki seçeneği de TAMAMLAR
sirala("ad", "Desc");     // TS2345 — yazım/büyük harf hatası anında

Genişletme kuralını hatırla: let yon = "asc" → string; const yon = "asc" → "asc". Nesne içindeki alanlar ise const ile tanımlansa da genişler — çünkü alan sonradan değiştirilebilir:

const istek = { yontem: "GET" };          // yontem: string — GENİŞLEDİ
fetch(url, istek);                        // yontem: "GET" bekleyen tipe UYMAZ

const istek2 = { yontem: "GET" } as const; // yontem: "GET" — dar kaldı
const istek3 = { yontem: "GET" as const }; // tek alanı daraltmak da mümkün

as const bu yüzden literal ağırlıklı yapılandırma nesnelerinin sadık arkadaşıdır. (Şablon literal tipleri — `kariyer:${string}` gibi desenli dizeler — 6. dersin utility konularıyla birlikte gelecek.)

04Daraltma araçları

Derleyici şu yapıları görünce tipi dal bazında daraltır:

function isle(deger: string | number | Date | null) {
  if (typeof deger === "string") {
    deger.toUpperCase();               // burada: string
  } else if (typeof deger === "number") {
    deger.toFixed(2);                  // burada: number
  } else if (deger instanceof Date) {
    deger.getTime();                   // burada: Date
  } else {
    deger;                             // burada: null — GERİYE KALAN
  }
}
  • typeof ilkeller için. 8. hattın 13. dersindeki tuzak tip sisteminde de yaşar: typeof deger === "object" dalına null da düşer — derleyici bunu bilir ve dalın tipini Nesne | null olarak tutar; null'u ayrıca elemen gerekir. Tuzağı bilen dil, tuzağı unutturmayan derleyiciye dönüşmüştür.
  • Eşitlik daraltması: deger === "hazir", a === b karşılaştırmaları iki tarafın tipini keser.
  • in operatörü nesne birleşimlerini üyeye göre ayırır: if ("yuzgec" in hayvan). Yapısal sistemde işe yarar ama kırılgandır — asıl kalıp 6. bölümdeki ayırt edici birleşimdir.
  • instanceof sınıf örnekleri için — ve en önemli sahnesi catch bloğudur:
class ApiHatasi extends Error {                  // 8. hat 17. dersten
  constructor(public durum: number, mesaj: string) { super(mesaj); }
}

try {
  await veriGetir("/api/basvurular");
} catch (e) {                                    // e: unknown (strict)
  e.message;                                     // TS18046 — kanıt yok!
  if (e instanceof ApiHatasi) {
    console.error(`HTTP ${e.durum}: ${e.message}`);  // ApiHatasi
  } else if (e instanceof Error) {
    console.error(e.message);                    // genel Error
  } else {
    console.error("Bilinmeyen hata:", e);        // throw "yazı" da mümkün!
  }
}

JavaScript'te her şey fırlatılabilir (throw "hata" bile); useUnknownInCatchVariables bu gerçeği tipe döker: catch değişkeni unknown'dır ve 8. hatta alışkanlık edindiğin instanceof merdiveni artık derleyici tarafından zorunlu kılınır. Hata sınıfı hiyerarşisi kurmanın (17. derste başlayan) karşılığı burada tam olarak ödenir.

05Doğruluk değeri: en sevilen ve en tehlikeli daraltıcı

function yazdir(mesaj: string | null) {
  if (mesaj) {
    mesaj.toUpperCase();     // string — null ELENDİ… ama yalnız o mu?
  }
}

Çalışır, derlenir, çoğu zaman doğrudur — ve 8. hattın 13. dersindeki tuzağı aynen içerir: if (mesaj) yalnız null'u değil, boş dizeyi de eler. string | null için bu genellikle zararsızdır; ama tip number | null olduğunda 0, string | undefined olduğunda "" sessizce "yok" muamelesi görür:

function indirimYaz(oran: number | null) {
  if (oran) { … }            // %0 indirim JS'de falsy — YANLIŞ dala gider
  if (oran !== null) { … }   // doğru: yalnız null elendi, 0 içeride
  if (oran != null) { … }    // null VE undefined'ı birlikte eler (bilinçli ==)
}

Karar kalıbı: "var mı?" sorusunun cevabı != null, "dolu mu?" sorusunun cevabı doğruluk değeri. İki soru farklıdır; hangisini sorduğunu bilerek yaz. ?? ve ?. bu ayrımın operatör hâlidir ve tiple kusursuz bütünleşir: oran ?? 0.2 ifadesinin tipi number'dır (null ihtimali düştü), kullanici?.adres?.sehir ifadesinin tipi string | undefined'dır — zincirin kısa devresi tipe yansır.

06Ayırt edici birleşim: durumun üretim kalıbı

Nesne birleşimlerini daraltmanın en sağlam yolu, her üyeye ortak adlı, literal tipli bir alan koymaktır. 8. hatta beş ekran durumunu (boş · yükleniyor · hata · dolu · kısmi) elle yönetmiştin; tiplenmiş hâli şudur:

type AramaDurumu =
  | { tur: "bos" }
  | { tur: "yukleniyor"; imza: AbortSignal }
  | { tur: "hata"; hata: ApiHatasi }
  | { tur: "dolu"; kayitlar: Basvuru[]; toplam: number };

function ciz(durum: AramaDurumu) {
  switch (durum.tur) {
    case "bos":        return bosEkran();
    case "yukleniyor": return iskelet();
    case "hata":       return hataKutusu(durum.hata);      // hata alanı BURADA var
    case "dolu":       return tablo(durum.kayitlar);       // kayitlar BURADA var
    default: {
      const kalan: never = durum;    // tüm durumlar tüketildiyse durum = never
      throw new Error(`Beklenmeyen durum: ${JSON.stringify(kalan)}`);
    }
  }
}

Bu kalıbın üç kazancı vardır:

  • İmkânsız durumlar temsil edilemez. "Hem yükleniyor hem kayıtlar dolu" gibi çelişkili hâller tek nesnede durum: boolean; kayitlar?: …; hata?: … yazınca mümkündür; birleşimde yazılamaz bile. Tip tasarımının altın kuralı: geçersiz hâli tipte imkânsız kıl, kontrol etmek zorunda kalma.
  • Her dalda doğru alanlar açılır. durum.hata'ya yalnız "hata" dalında erişilir; başka dalda derleme hatasıdır.
  • Tüketilmişlik denetimi: default'taki never ataması sayesinde birleşime yarın { tur: "kismi" } eklendiğinde bu switch derlenmez — TS2322: Type '{ tur: "kismi"; }' is not assignable to type 'never'. "Yeni durum ekledik, üç ekrandan birini güncellemeyi unuttuk" hatası sınıf olarak ortadan kalkar. Angular ve React'te durum yönetiminin (10-11. hat) bel kemiği bu kalıptır; Redux eylemleri, RxJS olayları hep ayırt edici birleşimdir.

07Kendi koruyucunu yazmak: x is T

Daraltma mantığı bir fonksiyona taşınacaksa, sıradan boolean dönüş yetmez — derleyici fonksiyonun içini çağrı yerinde takip etmez. Dönüş tipine tip yüklemi (type predicate) yazılır:

function apiHatasiMi(e: unknown): e is ApiHatasi {
  return e instanceof ApiHatasi;
}

catch (e) {
  if (apiHatasiMi(e)) e.durum;        // daraldı — yüklem çağrıya taşındı
}

// Doğrulama tarzı koruyucu — dış veri sınırında (5. derste büyüyecek):
function basvuruMu(v: unknown): v is Basvuru {
  return typeof v === "object" && v !== null &&
         "id" in v && typeof (v as { id: unknown }).id === "number";
}

Büyük uyarı: tip yüklemi bir beyandır — derleyici gövdenin yüklemi gerçekten kanıtladığını denetlemez. return true yazsan da imza geçerlidir ve yalan söyleyen koruyucu, as'ten daha sinsidir çünkü güvenilir görünür. Kural: koruyucu gövdesi kısa, mekanik ve alan alan kontrol eden kod olmalı; "herhâlde budur" sezgisi koruyucuya yazılmaz. Akrabası iddia fonksiyonudur: function dogrula(v: unknown): asserts v is Basvuru — dönerse tip daralır, dönmezse fırlatır; doğrulama katmanlarında (9. ders) kullanılır.

08Akış analizinin sınırları

Daraltma, derleyicinin kodu satır satır izleyebildiği yere kadar sürer. Üç yerde biter — üçü de günlük hayatta karşına çıkacak:

let deger: string | null = al();

// 1) GERİ ÇAĞRI SINIRI
if (deger !== null) {
  islemler.push(() => deger.toUpperCase());
  // TS18047 OLABİLİR: geri çağrı SONRA çalışacak; deger o ana dek değişebilir.
  // let yerine const kullanılırsa derleyici daraltmayı korur.
}

// 2) MUTASYON SIFIRLAR
if (deger !== null) {
  deger = belkiNull();       // daraltma bozuldu
  deger.toUpperCase();       // TS18047 — haklı olarak
}

// 3) FONKSİYON SINIRI
function dolu(x: string | null): boolean { return x !== null; }
if (dolu(deger)) deger.toUpperCase();
// TS18047 — sıradan boolean dönüş, bilgiyi çağrıya TAŞIMAZ (çözüm: x is string)

Pratik çıkarım üçlüdür: daraltılacak değeri const'a al (const d = deger; if (d !== null) … — const değişemeyeceği için daraltma geri çağrılarda bile korunur), daraltma sonrası mutasyondan kaçın ve yardımcı kontrolleri tip yüklemiyle yaz. Bunlar derleyici kaprisi değildir; derleyici, asenkron çalışacak geri çağrının eski kontrole güvenemeyeceğini senden iyi hatırlamaktadır — 8. hattın yarış koşulu dersinin tip sistemindeki yankısı.

09Mini alıştırma — Derleyiciyle birlikte düşün

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

  1. Ortak üye kuralı: number | string parametreli fonksiyonda toFixed, toUpperCase ve toString çağır. Hangisi neden geçti?
  2. Daraltmayı izle: aynı fonksiyonu typeof merdiveniyle böl ve her dalda parametrenin üzerine gelip tipini oku. else dalında ne kaldı?
  3. typeof null: string | Nokta | null tipli değeri typeof deger === "object" ile dalla; dalın içinde tipin Nokta | null kaldığını gör. Null'u nasıl elersin?
  4. 0 tuzağı: indirimYaz(oran: number | null) yaz; if (oran) ile if (oran != null) dallarında tipleri karşılaştır ve indirimYaz(0) için hangisinin yanlış davrandığını söyle.
  5. Zincir tipi: kullanici?.adres?.sehir ifadesinin tipini oku; sonra ?? "bilinmiyor" ekle ve tipin nasıl değiştiğini gör.
  6. catch merdiveni: ApiHatasi extends Error kur; bir try/catch içinde önce e.message'a doğrudan eriş (TS18046), sonra instanceof merdivenini kur. Son else'e hangi değerler düşebilir?
  7. Ayırt edici birleşim: bu dersteki AramaDurumu'nu yaz, switch + default: never kur. Sonra birleşime { tur: "kismi" } ekle — hata tam olarak hangi satırda, ne diyerek çıktı? Bu hatanın üretimde önlediği senaryoyu bir cümleyle yaz.
  8. İmkânsız hâl: aynı durumu tek nesneyle (yukleniyor: boolean; kayitlar?: …; hata?: …) modelle ve "hem hata hem dolu" nesnesini yaz — derleyici itiraz etti mi? Birleşim modelinde aynı şeyi yazmayı dene.
  9. Yalancı koruyucu: gövdesi return true olan e is ApiHatasi koruyucusu yaz ve e.durum'a eriş. Derleyici seni korudu mu? Çalıştırınca ne olur? Çıkardığın kural ne?
  10. Akış sınırı: let deger: string | null ile 8. bölümdeki üç senaryoyu tek tek yeniden üret; her birinde hatayı gör, sonra const'a alma ve tip yüklemi çözümleriyle geçir.

10Kendini sına

Ayırt edici birleşim (discriminated union) nedir? "Tek nesne + isteğe bağlı alanlar" modeline neden tercih edilir?

Her üyesi, ortak adlı ve literal tipli bir ayırt edici alan (tur: "hata" gibi) taşıyan nesne birleşimidir; switch (durum.tur) yazıldığında derleyici her dalda ilgili üyeye daraltır. Üç üstünlüğü vardır. (1) İmkânsız hâller yazılamaz: tek nesnede yukleniyor: boolean + isteğe bağlı kayitlar/hata alanları "hem yükleniyor hem hatalı" gibi çelişkileri temsil edebilir ve her kullanım yeri savunmacı kontrole mahkûm olur; birleşimde geçersiz kombinasyon tipte var olmadığı için kontrol de gerekmez — "geçersiz durumu imkânsız kıl" ilkesi. (2) Alan erişimi dal bazında güvenlidir: durum.hata'ya yalnız hata dalında erişilir. (3) Tüketilmişlik denetimi: default dalında kalan değeri never tipli değişkene atarsan, birleşime yeni üye eklendiğinde bu atama derleme hatası verir — "yeni durumu üç ekrandan birinde unutmak" hatası derleme anına çekilir. Ekran durumları, Redux eylemleri, RxJS olayları ve API yanıt modelleri bu kalıpla yazılır; 10-11. hatların durum yönetimi buna dayanır.

if (deger) daraltması hangi durumda hataya yol açar? Doğru kalıp nedir?

Doğruluk değeri daraltması null ve undefined'la birlikte tüm falsy değerleri eler: 0, "", NaN, false. Tip string | null gibi "falsy değeri anlamlı olmayan" bir birleşimse sorun çıkmaz; ama number | null'da 0 (yüzde sıfır indirim, sıfır bakiye) ve string | undefined'da boş dize meşru değerlerdir ve if (deger) onları sessizce "yok" dalına atar — derleyici de itiraz etmez çünkü daraltma tip açısından tutarlıdır; hata mantık düzeyindedir. Doğru kalıp soruyu netleştirmektir: "değer var mı?" sorusu deger != null ile sorulur (bilinçli çift eşittir: null ve undefined'ı birlikte eler, 0 ve boş dizeyi içeride tutar); "değer dolu/anlamlı mı?" sorusu doğruluk değeriyle sorulabilir. ?? aynı ayrımın operatörüdür: oran ?? 0.2 yalnız null/undefined'da varsayılana düşer, oran || 0.2 ise 0'da da düşer — 8. hattın 13. dersindeki kural tip sisteminde aynen yaşar ve ?? sonrası tipten null ihtimalinin düşmesi, derleyicinin bu ayrımı anladığının kanıtıdır.

catch bloğunda yakalanan değerin tipi neden unknown'dır ve nasıl ele alınır?

Çünkü JavaScript'te throw her değeri fırlatabilir: Error türevleri dışında dize, sayı, düz nesne, hatta undefined. Ayrıca hata, senin kodundan değil üçüncü taraf kütüphaneden ya da çalışma ortamından da gelebilir; tip sistemi hangi tipin fırlayacağını izlemez (Java'daki checked exceptions TypeScript'te yoktur). useUnknownInCatchVariables (strict ailesinden) bu gerçeği tipe döker: catch (e) içinde e: unknown'dır ve e.message'a doğrudan erişim TS18046 verir. Doğru ele alış, daraltma merdivenidir: önce en özel kendi hata sınıfların (e instanceof ApiHatasi — 8. hat 17. derste kurulan sınıf burada meyvesini verir: e.durum gibi alanlar daralınca açılır), sonra e instanceof Error ile genel hata, en sonda "bilinmeyen değer" dalı. Tekrarlanan merdiven bir tip yüklemine (e is ApiHatasi) ya da "her şeyi Error'a çevir" yardımcısına çıkarılır. Yanlış ele alış catch (e: any) yazmaktır — derleyicinin verdiği korumayı kapı önünde geri teslim etmek.

Tip yüklemi (x is T) ne işe yarar, riski nedir? Daraltma hangi durumlarda "kaybolur"?

Sıradan boolean döndüren kontrol fonksiyonu, çağrı yerinde daraltma yapmaz — derleyici fonksiyon gövdesini çağrı noktasında izlemez. Dönüş tipini e is ApiHatasi biçiminde yazmak, "true dönersem argüman bu tiptedir" bilgisini imzaya taşır ve if (apiHatasiMi(e)) artık daraltır. Risk: yüklem bir beyandır, derleyici gövdenin onu kanıtladığını doğrulamaz; return true yazan bir koruyucu tip sistemini sessizce zehirler ve güvenilir göründüğü için as'ten sinsidir. Kural: koruyucu gövdesi alan alan, mekanik kontrol yapmalı. İkinci soru — daraltma üç sınırda kaybolur: (1) geri çağrı sınırı: let değişkenin daraltması, sonra çalışacak bir fonksiyonun içinde korunmaz çünkü değer o ana dek değişebilir; çözüm değeri const'a almaktır. (2) mutasyon: daraltmadan sonra değişkene yeni atama yapılırsa analiz haklı olarak sıfırlanır. (3) fonksiyon sınırı: kontrolü başka fonksiyona taşımak bilgiyi taşımaz — tip yüklemi tam bu boşluk için vardır. Bu davranışlar kapris değildir; özellikle geri çağrı kuralı, asenkron kodda eski kontrole güvenmenin gerçek bir yarış koşulu hatası olduğunu hatırlatır.

11Sınav köşesi

Yazılıda nasıl sorulur

(1) Union type nedir; üzerinde hangi üyelere erişilebilir? (2) Literal type nedir; ne işe yarar? (3) Type narrowing nedir; hangi yapılarla yapılır? (4) typeof, instanceof, in ile daraltma örnekleri. (5) Discriminated union nedir; nasıl kurulur? (6) Exhaustiveness check (never ile) nasıl yapılır? (7) Type guard / type predicate (x is T) nedir? (8) Assertion function (asserts) nedir? (9) catch değişkeni neden unknown'dır? (10) Truthiness narrowing'in riski nedir? (11) as const daraltmaya nasıl yardım eder? (12) Intersection type ile union type farkı.

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

Düzen: (1) Çerçeve — "günlük TypeScript iki hamledir: olası hâlleri birleşimle yazmak, her dalda daraltmak. Birleşimde yalnız ortak üyelere erişilir; gerisi kanıt ister ve kanıtı derleyici if'lerimden okur — kontrol akışı analizi." (2) Araçlar — "typeof ilkellere (null'un 'object' çıkması tipte de yaşar), instanceof sınıflara — özellikle catch'te, çünkü değişken unknown'dır ve hata sınıfı merdiveni zorunludur; in üye kontrolüne." (3) Üretim kalıbı — "durumları ayırt edici birleşimle modellerim: imkânsız hâller yazılamaz, her dalda doğru alanlar açılır, default'taki never ataması yeni durum eklenince derlemeyi kırar — ekran/eylem modellemenin bel kemiği." (4) Kıdem vuruşu — "doğruluk değeri daraltması 0 ve boş dizeyi de eler; 'var mı' sorusunu != null ile sorarım. Tip yüklemi beyandır, gövdesini derleyici doğrulamaz — koruyucular mekanik yazılır. Daraltma geri çağrıda ve mutasyonda kaybolur; değeri const'a alırım."