Mert Özen Her Satırda Daha İleri
Frontend TypeScript Temel — #09

TypeScript'te Narrowing: Union'ları Daraltmak ve Control Flow Analysis

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript'te Narrowing: Union'ları Daraltmak ve Control Flow Analysis

TypeScript'in control flow analysis ile union'ları nasıl daralttığını; typeof, truthiness, eşitlik, in ve instanceof kontrollerini, discriminated union'ları ve never ile exhaustive check'i görüyoruz.

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

Önce Tahmin Et

Bir önceki yazıda union'larla çalışırken typeof ile bir kez daraltma yaptık. Şimdi aynı aracı biraz daha zorlayalım. Bu kod derlenir mi? Hata verirse hangi satırda ve neden?

function printLength(value: string | string[] | null) {
  if (typeof value === "object") {
    console.log(value.length);
  }
}

Mantık sağlam görünüyor: string'ler "string" döner, array'ler "object". O hâlde if bloğunun içinde elimizde bir array olmalı, değil mi? Cevabı typeof bölümünde vereceğiz.

Narrowing ve Control Flow Analysis

Narrowing, bir union'ı kodun belli bir noktasında daha dar bir tipe indirmektir. Bunun için TypeScript'e yeni bir şey öğretmen gerekmez; JavaScript'te zaten yazdığın if, switch, return ve atamaları okur. Buna control flow analysis denir: derleyici kodunu bir akış şeması gibi dolaşır ve her noktada bir değişkenin hangi tipte olabileceğini hesaplar.

function formatPrice(price: number | string) {
  // price: number | string
  if (typeof price === "string") {
    price = Number.parseFloat(price);
    // price: number
  }
  // price: number
  return price.toFixed(2);
}

console.log(formatPrice("19.5")); // 19.50
console.log(formatPrice(7)); // 7.00

Fonksiyonun girişinde price iki tipten biri olabilir. if bloğunun içinde TypeScript onun string olduğunu bilir ve Number.parseFloat'a vermene izin verir. Blokta yeni bir sayı atadığın için price o satırdan sonra number olur. Bloğun altında iki yol birleşir: ya blok çalıştı ve değer number'a çevrildi, ya da blok hiç çalışmadı çünkü değer zaten number'dı. Her iki yolda da sonuç number, bu yüzden toFixed güvenle çağrılabilir.

Editörde değişkenin üzerine gelerek bu hesabı her satırda görebilirsin. Narrowing'i öğrenmenin en iyi yolu da budur: bir kontrol yaz, sonra hemen altında değişkenin tipine bak.

typeof ile Daraltma

JavaScript'in typeof operatörü sekiz farklı string döndürür: "string", "number", "bigint", "boolean", "symbol", "undefined", "object" ve "function". TypeScript bunların her birini tanır ve union'dan uygun member'ları seçer.

Baştaki sorunun cevabı JavaScript'in eski bir tuhaflığında saklı: typeof null da "object" döner. TypeScript bunu bilir; if bloğunun içinde value'nun tipini string[] | null olarak hesaplar ve şöyle itiraz eder:

error TS18047: 'value' is possibly 'null'.

Biz null'ı unutmuştuk, derleyici unutmadı. Çözüm, null'ı ayrıca elemek:

function printLength(value: string | string[] | null) {
  if (value !== null && typeof value === "object") {
    // value: string[]
    console.log(value.length);
  }
}

Array'ler için daha okunaklı bir yol da var: Array.isArray(value). TypeScript bu fonksiyonu da tanır ve true dalında değeri array'e daraltır.

Truthiness ile Daraltma

JavaScript serilerinden hatırlarsın: if içine yazılan değer boolean'a çevrilir ve false, 0, "", null, undefined ve NaN falsy sayılır. TypeScript bu kuralı da narrowing için kullanır:

function printLength(value: string | string[] | null) {
  if (value) {
    // value: string | string[]
    console.log(value.length);
  } else {
    // value: string | null
    console.log("nothing to measure");
  }
}

if (value) bloğunda null elendi, çünkü null asla truthy olamaz. else dalına dikkat et: orada string[] yok, çünkü array'ler boş olsalar bile her zaman truthy'dir. Ama string hâlâ orada duruyor. Sebebi, boş string'in falsy olması; else'e gelen değer null olabileceği gibi "" de olabilir. Truthiness kısa ve kullanışlıdır, ama neyi elediğini her zaman hesaba katmalısın. Bunu tuzaklar bölümünde yeniden konuşacağız.

Eşitlik Kontrolleri

=== ve !== de daraltır. En sık kullanımı belirli bir değeri elemektir, ama bazen daha ilginç sonuçlar verir:

function compare(a: string | number, b: string | boolean) {
  if (a === b) {
    // a: string, b: string
    return a.toUpperCase() === b.toUpperCase();
  }
  return false;
}

a ile b birbirine eşitse ikisi de aynı tipte olmalı. İki union'ın tek ortak member'ı string olduğu için TypeScript bloğun içinde ikisini de string'e daraltır.

Gevşek eşitliğin, yani =='nin bir faydalı kullanımı var: x == null hem null'ı hem de undefined'ı yakalar. TypeScript bunu da bilir:

function greet(name?: string | null) {
  if (name == null) {
    return "Hello, stranger!";
  }
  // name: string
  return `Hello, ${name}!`;
}

in ve instanceof

Object union'larında bir property'nin var olup olmadığını in operatörüyle sorabilirsin:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    // animal: Fish
    animal.swim();
  } else {
    // animal: Bird
    animal.fly();
  }
}

"swim" in animal doğruysa elimizdeki Fish'tir, değilse Bird. Class'lardan üretilmiş değerlerde ise instanceof kullanırsın:

function formatDate(value: Date | string) {
  if (value instanceof Date) {
    // value: Date
    return value.toISOString();
  }
  // value: string
  return value;
}

İkisinin farkı şu: in object'in şekline bakar, instanceof ise onun hangi constructor'dan geldiğine, yani prototype zincirine. TypeScript'te tiplerin büyük çoğunluğu düz object'ler olduğu için in'i daha sık kullanırsın; instanceof ise Date, Error ve kendi class'ların için doğru araçtır.

Discriminated Union

Bir önceki yazıda Circle | Square union'ında shape.radius'a erişemediğimizi görmüştük. in ile çözebilirdik, ama daha temiz bir desen var: her member'a aynı isimde, değeri literal type olan bir property vermek.

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
type Shape = Circle | Square;

function area(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      // shape: Circle
      return Math.PI * shape.radius ** 2;
    case "square":
      // shape: Square
      return shape.size ** 2;
  }
}

console.log(area({ kind: "square", size: 3 })); // 9

kind property'si her member'da var, ama her birinde farklı bir literal. TypeScript shape.kind'ın değerine bakarak hangi member'la çalıştığını kesin olarak bilir. Bu ortak property'ye discriminant, desenin kendisine de discriminated union denir. İsim olarak kind, type ya da status sık görülür; önemli olan, union'daki her member'da aynı ismin bulunması ve değerlerin birbirinden farklı olması.

Bu desen TypeScript'te gerçek hayatın her yerinde karşına çıkar: bir isteğin durumu ("loading", "success", "error"), bir formdaki adımlar, bir uygulamada işlenen mesaj türleri. Her biri kendi verisini taşır ve sen switch ile her duruma ayrı ayrı bakarsın.

never ile Exhaustive Check

Discriminated union'ın bir de güvenlik kemeri var. Yarın biri Shape'e üçüncü bir member eklerse, switch'lerin hepsinin güncellenmesi gerekir. Bunu unutmamak için default dalına bir kontrol koyarsın:

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default: {
      // Hata: Type 'Triangle' is not assignable to type 'never'.
      const unhandled: never = shape;
      return unhandled;
    }
  }
}

Daha önce gördüğümüz never, hiçbir değerin sahip olamayacağı tiptir. Bütün member'lar ele alındıysa default dalına gelindiğinde shape'in tipi never olur ve atama sorunsuz geçer. Bir member unutulduysa, shape o member'ın tipini taşır ve atama hata verir. Üstelik mesaj, neyi unuttuğunu da söyler: Triangle. case "triangle" dalını eklediğin anda hata kaybolur.

Fonksiyonun dönüş tipini açıkça yazmak da benzer bir koruma sağlar. default dalı olmasaydı TypeScript bu kez Function lacks ending return statement and return type does not include 'undefined'. diyerek fonksiyonun bazı yollarda değer döndürmediğini söylerdi. never ataması ise hatayı tam olarak unutulan durumun üzerine koyduğu için daha açıklayıcıdır.

Sık Düşülen Tuzaklar

Birinci tuzak, truthiness'in geçerli değerleri de yutması:

function formatCount(count: number | undefined) {
  if (!count) {
    return "no data";
  }
  return `${count} items`;
}

console.log(formatCount(0)); // no data

Tip denetimi açısından her şey doğru; TypeScript hiçbir hata vermez. Ama 0 geçerli bir sayı ve "veri yok" anlamına gelmiyor. Asıl kastettiğin "değer yoksa" ise bunu açıkça yaz: if (count === undefined). Aynı tuzak boş string için de geçerlidir.

İkinci tuzak, typeof x === "object"'in null'ı da içermesi. Bunu yazının başında gördük; "object" kontrolü yaptığın her yerde null'ı da düşün.

Üçüncü tuzak, daraltmanın callback'lerin içine her zaman taşınmaması:

function scheduleTitle(title: string | undefined) {
  if (title !== undefined) {
    // Hata: 'title' is possibly 'undefined'.
    setTimeout(() => console.log(title.toUpperCase()), 100);
  }
  title = undefined;
}

Callback daha sonra çalışacak ve o zamana kadar title değişmiş olabilir; gerçekten de son satır ona undefined atıyor. TypeScript bu yüzden if'teki daraltmayı callback'in içine taşımaz. TS 5.4'ten beri derleyici bu konuda daha akıllı: değişkene callback oluşturulduktan sonra hiç atama yapılmıyorsa daraltma korunur. Yani son satırı silersen hata da kaybolur. Pratikteki çözüm, parametrelere ve let değişkenlerine sonradan atama yapmamak ya da daraltılmış değeri yeni bir const'a kopyalayıp callback'te onu kullanmaktır.

Küçük Bir Deneme

★☆☆ Tarayıcının konsolunda ya da Node'da typeof null, typeof [] ve typeof (() => 1) ifadelerini çalıştır. Sonra yazıdaki ilk printLength örneğini editörde aç ve if içindeki value'nun üzerine gel.

★★☆ Type'ı ne, tahmin et: compare fonksiyonunda if (a === b) yerine if (a !== b) yazarsan, if bloğunun içinde a'nın tipi ne olur? Daraltma olur mu?

★★☆ never ile exhaustive check yaptığımız örneğe case "triangle" dalını ekle ve hatanın kaybolduğunu gör. Sonra Shape'e kendi seçtiğin dördüncü bir şekil ekle ve hata mesajının ne söylediğini oku.

★★★ Bir isteğin durumunu tarif eden bir discriminated union yaz: "loading", "success" (bir data: string[] taşısın) ve "error" (bir message: string taşısın). Her durum için bir string döndüren render fonksiyonunu switch ve never kontrolüyle yaz. Sonra union'a "idle" durumunu ekle ve derleyicinin seni nereye yönlendirdiğine bak.

Bu yazıdaki bütün örneklerde fonksiyon yazdık ama fonksiyonların kendi tiplerine pek dokunmadık. Bir sonraki yazıda tam olarak onlara bakacağız: optional, default ve rest parametreler, bir fonksiyonun tipini tek başına yazmak, void dönüşün inceliği, overload'lar ve this parametresi.