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

TypeScript Type Guard'ları: Type Predicate ve Assertion Function

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript Type Guard'ları: Type Predicate ve Assertion Function

x is T ile type predicate, asserts ile assertion function yazıyor; TypeScript 5.5'in inferred type predicates özelliğini ve filter ile array daraltmayı tuzaklarıyla birlikte görüyoruz.

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

Önce Tahmin Et

Narrowing'i Temel seride gördük. Şimdi o kontrolü bir fonksiyonun içine taşıyalım:

type Cat = { kind: "cat"; meow(): void };
type Dog = { kind: "dog"; bark(): void };

function isCat(pet: Cat | Dog): boolean {
  return pet.kind === "cat";
}

function speak(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow();
  }
}

isCat doğru çalışıyor, true döndüğünde elimizdeki gerçekten bir kedi. Peki bu kod derlenir mi? Derlenmiyorsa hangi satırda takılır?

Narrowing Fonksiyon Sınırında Neden Kaybolur?

Kod derlenmez. pet.meow() satırında şu hatayı alırsın:

error TS2339: Property 'meow' does not exist on type 'Cat | Dog'.
  Property 'meow' does not exist on type 'Dog'.

Sebebi şu: TypeScript speak'i denetlerken isCat'in içine bakmaz, sadece imzasına bakar. İmza da "bu fonksiyon bir boolean döndürür" diyor. true ya da false, ikisi de pet hakkında hiçbir şey söylemiyor. if (pet.kind === "cat") kontrolünü doğrudan speak'in içine yazsaydın narrowing çalışırdı; aynı kontrolü bir fonksiyona taşıdığın anda bilgi fonksiyonun sınırında kayboldu.

Bir binanın girişindeki görevliyi düşün. Görevli kimliğine bakıp sadece "tamam" derse, içerideki kimse senin kim olduğunu bilmez. Bir de üzerinde adının ve yetkinin yazdığı bir yaka kartı verebilir. boolean dönen bir fonksiyon "tamam" diyen görevlidir. Type guard ise yaka kartını veren.

Type Predicate: x is T

Yaka kartının TypeScript'teki adı type predicate. Dönüş tipine boolean yerine parameterName is Type yazarsın:

type Cat = { kind: "cat"; meow(): void };
type Dog = { kind: "dog"; bark(): void };

function isCat(pet: Cat | Dog): pet is Cat {
  return pet.kind === "cat";
}

function speak(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow(); // pet: Cat
  } else {
    pet.bark(); // pet: Dog
  }
}

speak({ kind: "dog", bark: () => console.log("Woof") }); // Woof

Çalışma zamanında pet is Cat yine düz bir boolean'dır; derlenmiş JavaScript'te bu annotation silinir. Değişen, derleyicinin çağrı yerindeki bilgisi. isCat(pet) true dönerse pet, if bloğunda Cat'e daralır. else bloğunda ise TypeScript, union'dan Cat'i çıkarır ve geriye Dog kalır.

Type guard'lar en çok, hakkında hiçbir şey bilmediğin değerlerde işe yarar. Parametre unknown olabilir:

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every((item) => typeof item === "string");
}

const saved: unknown = JSON.parse('["ts", "js"]');

if (isStringArray(saved)) {
  console.log(saved.join(", ")); // ts, js
}

JSON.parse sana any döndürür; burada onu bilerek unknown bir değişkene koyduk ki kontrol etmeden kullanamayalım. isStringArray çalışma zamanında gerçekten kontrol ediyor, TypeScript de bu kontrolün sonucunu tipe çeviriyor. Kontrol ve tip bilgisi tek bir yerde birleşiyor.

Predicate Bir Söz, Kanıt Değil

Burada önemli bir uyarı var. TypeScript, predicate'in gövdesinin doğru olup olmadığını kontrol etmez. value is number yazdığında derleyiciye bir söz veriyorsun ve derleyici sana güveniyor:

function isNumber(value: unknown): value is number {
  return typeof value === "string"; // yanlış kontrol, derleyici fark etmez
}

const input: unknown = "42";

if (isNumber(input)) {
  console.log(input.toFixed(2)); // TypeError: input.toFixed is not a function
}

Kod tertemiz derlenir ve çalışma zamanında patlar. Bu açıdan type predicate, Temel seride gördüğümüz as'a benzer: ikisi de "bu konuda beni dinle" demenin yoludur. Farkı, kontrolün tek bir fonksiyonda toplanmasıdır. O yüzden type guard'larını kısa tut, ne kontrol ettiklerini okuyunca anlaşılsın ve mümkünse testlerini yaz. Bir hata yaparsan, onu kullanan her yer yanlış bir tiple çalışır.

Assertion Function: asserts x is T

Type predicate bir if ile birlikte çalışır. Bazen ise if yazmak istemezsin: değer beklediğin gibi değilse devam etmenin anlamı yoktur ve hata fırlatmak istersin. Bunun için assertion function kullanılır:

type User = { id: number; name: string };

function assertIsDefined<T>(value: T, label: string): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error(`${label} is missing`);
  }
}

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

const users = new Map<number, User>([[1, { id: 1, name: "Ada" }]]);

const ada = users.get(1); // User | undefined
assertIsDefined(ada, "user 1");
console.log(ada.name); // Ada

const limit: unknown = 20;
assert(typeof limit === "number", "limit must be a number");
console.log(limit * 2); // 40

const ghost = users.get(2);
assertIsDefined(ghost, "user 2"); // Error: user 2 is missing

Assertion function bir değer döndürmez. Verdiği söz şudur: "Hata fırlatmadan dönersem, bu koşul doğrudur." TypeScript de çağrıdan sonraki satırlarda bu sözü geçerli kabul eder. assertIsDefined(ada, ...) satırından sonra ada artık User'dır, undefined ihtimali kalmamıştır. Burada önceki yazıda gördüğümüz NonNullable'ı da kullandık.

İkinci biçim, asserts condition, belirli bir type yerine bir koşulu garanti eder. assert(typeof limit === "number", ...) çağrısından sonra TypeScript, sanki aynı koşulu bir if içinde kontrol etmişsin gibi daraltma yapar. Son satır ise bu fonksiyonların çalışma zamanı tarafı: ghost yok, assertion hata fırlatır ve program orada durur.

TypeScript 5.5: Inferred Type Predicates

Baştaki örneğe dönelim. TypeScript 5.5'ten beri, isCat'teki : boolean annotation'ını silmen yeterli:

type Cat = { kind: "cat"; meow(): void };
type Dog = { kind: "dog"; bark(): void };

function isCat(pet: Cat | Dog) {
  return pet.kind === "cat";
}

function speak(pet: Cat | Dog) {
  if (isCat(pet)) {
    pet.meow();
  }
}

function isNonEmptyString(value: unknown) {
  return typeof value === "string" && value.length > 0;
}

function shout(value: string | number) {
  if (isNonEmptyString(value)) {
    // Hata: Property 'toUpperCase' does not exist on type 'string | number'.
    console.log(value.toUpperCase());
  }
}

Dönüş tipini sen yazmadığında TypeScript fonksiyonun gövdesine bakar ve pet.kind === "cat" ifadesinden pet is Cat predicate'ini kendisi çıkarır. Baştaki hatanın sebebi aslında bizim yazdığımız : boolean'dı; o annotation, TypeScript'in çıkarabileceği daha zengin bir bilgiyi ezdi.

Ama her boolean döndüren fonksiyon bir predicate'e dönüşmez. Kural şu: fonksiyon true döndüğünde değer o type'tadır, false döndüğünde de o type'ta değildir; ikisi birden doğru olmalı. isNonEmptyString bu kurala uymuyor. false dönmesi değerin string olmadığı anlamına gelmez; boş bir string de false verir. Bu yüzden TypeScript bir predicate çıkarmaz, dönüş tipi boolean kalır ve shout içinde daraltma olmaz. Böyle bir durumda predicate'i açıkça yazmak senin kararın olur. Çıkarımın çalışması için ayrıca fonksiyonun açık bir dönüş tipi olmaması, tek bir return ifadesi içermesi ve parametresini değiştirmemesi gerekir.

filter ile Daraltma

Bu özelliğin en çok hissedildiği yer array'lerin filter method'u. filter'ın bir overload'u, callback bir type predicate ise sonuç array'inin tipini daraltır. TypeScript 5.5'ten önce callback'leri bunun için elle işaretlemen gerekiyordu. Artık çoğu zaman gerekmiyor:

const values = [1, null, 2, undefined, 3];

const numbers = values.filter((v) => v !== null && v !== undefined); // number[]
const shorter = values.filter((v) => v != null); // number[]

const truthy = values.filter(Boolean); // (number | null | undefined)[]
const alsoTruthy = values.filter((v) => !!v); // (number | null | undefined)[]

const explicit = values.filter((v): v is number => typeof v === "number"); // number[]

console.log(numbers.map((n) => n * 10)); // [ 10, 20, 30 ]

İlk iki satırda TypeScript callback'ten v is number predicate'ini çıkarıyor ve sonuç number[] oluyor. filter(Boolean) ise popüler bir JavaScript kalıbı olmasına rağmen tipi daraltmaz: Boolean fonksiyonunun imzası bir predicate değil. (v) => !!v de daraltmaz, çünkü az önceki kurala takılır: 0 da falsy'dir, yani false dönmesi değerin sayı olmadığını kanıtlamaz. Bu iki durumda ya karşılaştırmayı açıkça yaz ya da son örnekteki gibi predicate'i kendin ver.

Sık Düşülen Tuzaklar

// Hata: A type predicate's type must be assignable to its parameter's type.
function isId(value: string): value is number {
  return true;
}

const assertArrow = (condition: unknown, message: string): asserts condition => {
  if (!condition) throw new Error(message);
};

function check(value: string | null) {
  // Hata: Assertions require every name in the call target to be declared with an explicit type annotation.
  assertArrow(value, "value is required");
}

const assertTyped: (condition: unknown, message: string) => asserts condition = (condition, message) => {
  if (!condition) throw new Error(message);
};

function checkTyped(value: string | null) {
  assertTyped(value, "value is required");
  console.log(value.length);
}

Birincisi, predicate'teki type, parametrenin type'ının bir alt kümesi olmalı. string alan bir parametre için "bu aslında bir number" diyemezsin; TS2677 hatası alırsın. Predicate bir daraltmadır, tip değiştirme değil.

İkincisi, arrow function ile yazılmış assertion'lar. Bir assertion function'ı bir değişkene atayıp o değişken üzerinden çağırıyorsan, değişkenin tipini açıkça yazman gerekir. Yoksa TS2775 hatası alırsın. Sebebi, TypeScript'in akış analizini yaparken çağrılan şeyin tipini önceden bilmek istemesi. En basit çözüm assertion'ları function bildirimiyle yazmak; arrow function'da ısrar ediyorsan assertTyped örneğindeki gibi değişkene tip ver.

Üçüncüsü yukarıda gördüğümüz yalan söyleyen predicate. Derleyici predicate'lerine güvenir; o güveni boşa çıkarmamak senin sorumluluğun.

Küçük Bir Deneme

★☆☆ type Circle = { kind: "circle"; radius: number } ve type Square = { kind: "square"; size: number } için bir isCircle type guard'ı yaz. Sonra dönüş tipini sil ve TypeScript'in predicate'i kendisinin çıkarıp çıkarmadığını editörde fonksiyonun üzerine gelerek kontrol et.

★★☆ Tahmin et: const ids = ["1", "", "3"].filter((s) => s.length > 0); satırında ids'in type'ı nedir? Peki const xs = [1, "a", 2].filter((x) => typeof x === "number"); için?

★★★ unknown bir değerin { id: number; title: string } şeklinde olduğunu kontrol eden bir isTodo type guard'ı yaz. Sonra aynı kontrolü bir assertIsTodo assertion function'ı olarak yaz ve ikisini kullanırken kodun nasıl farklılaştığını karşılaştır.

Son alıştırmadaki gibi type guard'lara en çok dış dünyadan gelen verilerde ihtiyaç duyarsın, ve o veri çoğu zaman bir ağ isteğinin sonunda, asenkron olarak gelir. Bir sonraki yazıda Promise'lerin ve async fonksiyonların tiplerini konuşacak, fetch ile gelen bir cevabı bu yazıdaki araçlarla güvenli hâle getireceğiz.