İçeriğe geç
TypeScript

TypeScript Type Narrowing: Tip Daraltma Teknikleri

Tarık Tunç
TypeScript Type Narrowing: Tip Daraltma Teknikleri

TypeScript Type Narrowing: Tip Daraltma Teknikleri

Type narrowing, TypeScript'in bir değişkenin tipini daha spesifik bir tipe daraltma sürecidir. Kontrol akışı analizi sayesinde TypeScript, koşullu ifadelerden sonra değişkenlerin tiplerini otomatik olarak daraltır. Bu yazıda tip daraltmanın tüm tekniklerini kapsamlı örneklerle inceleyeceğiz.

typeof Guard

En temel tip daraltma yöntemi, JavaScript'in typeof operatörünü kullanmaktır:

function formatla(deger: string | number | boolean): string {
  if (typeof deger === "string") {
    // TypeScript burada deger'in string olduğunu bilir
    return deger.toUpperCase();
  }

  if (typeof deger === "number") {
    // Burada number
    return deger.toFixed(2);
  }

  // Burada boolean
  return deger ? "Evet" : "Hayır";
}

// typeof ile kontrol edilebilen tipler:
// "string", "number", "boolean", "symbol", "undefined",
// "object", "function", "bigint"

function islemYap(girdi: string | number | null): number {
  if (typeof girdi === "string") {
    return parseInt(girdi, 10);
  }
  if (typeof girdi === "number") {
    return girdi;
  }
  // girdi: null
  return 0;
}

instanceof Guard

Sınıf tabanlı tip daraltma için instanceof operatörü kullanılır:

class ApiHatasi extends Error {
  constructor(public statusCode: number, mesaj: string) {
    super(mesaj);
  }
}

class DogrulamaHatasi extends Error {
  constructor(public alanlar: Record<string, string>) {
    super("Doğrulama hatası");
  }
}

function hataYonet(hata: Error): string {
  if (hata instanceof ApiHatasi) {
    return `API Hatası (${hata.statusCode}): ${hata.message}`;
  }

  if (hata instanceof DogrulamaHatasi) {
    const hatalar = Object.entries(hata.alanlar)
      .map(([alan, mesaj]) => `${alan}: ${mesaj}`)
      .join(", ");
    return `Doğrulama Hatası: ${hatalar}`;
  }

  return `Bilinmeyen Hata: ${hata.message}`;
}

// Date kontrolü
function tarihFormatla(deger: string | Date): string {
  if (deger instanceof Date) {
    return deger.toLocaleDateString("tr-TR");
  }
  return new Date(deger).toLocaleDateString("tr-TR");
}

in Operatörü ile Daraltma

interface Kus {
  tur: string;
  uc(): void;
}

interface Balik {
  tur: string;
  yuz(): void;
}

function hareketEt(hayvan: Kus | Balik): void {
  if ("uc" in hayvan) {
    hayvan.uc(); // TypeScript: Kus
  } else {
    hayvan.yuz(); // TypeScript: Balik
  }
}

// Daha karmaşık örnek
interface AdminKullanici {
  rol: "admin";
  yetkiler: string[];
  superAdmin: boolean;
}

interface NormalKullanici {
  rol: "kullanici";
  abonelikTipi: "ucretsiz" | "premium";
}

type Kullanici = AdminKullanici | NormalKullanici;

function kullaniciBilgisi(kullanici: Kullanici): string {
  if ("yetkiler" in kullanici) {
    return `Admin: ${kullanici.yetkiler.join(", ")}`;
  }
  return `Kullanıcı: ${kullanici.abonelikTipi} üye`;
}
İpucu: in operatörü, interface'lerde discriminant property olmadığında tip daraltma için çok kullanışlıdır. Ancak mümkünse discriminated union pattern'ini tercih edin; daha açık ve güvenlidir.

Custom Type Guards

Kendi tip koruma fonksiyonlarınızı is anahtar kelimesi ile tanımlayabilirsiniz:

// User-defined type guard
interface Araba {
  tur: "araba";
  tekerlekSayisi: 4;
  bagajHacmi: number;
}

interface Motosiklet {
  tur: "motosiklet";
  tekerlekSayisi: 2;
  kasaTipi: string;
}

type Arac = Araba | Motosiklet;

// is keyword ile type guard
function arabaDir(arac: Arac): arac is Araba {
  return arac.tur === "araba";
}

function aracBilgisi(arac: Arac): string {
  if (arabaDir(arac)) {
    return `Araba - Bagaj: ${arac.bagajHacmi}L`;
  }
  return `Motosiklet - Kasa: ${arac.kasaTipi}`;
}

// Null check type guard
function doluDeger<T>(deger: T | null | undefined): deger is T {
  return deger !== null && deger !== undefined;
}

const degerler = [1, null, 2, undefined, 3, null];
const temizdegerler = degerler.filter(doluDeger);
// number[] - null ve undefined filtrelendi!

// API yanıt type guard
interface ApiBasarili<T> {
  basarili: true;
  data: T;
}

interface ApiHata {
  basarili: false;
  hata: string;
}

type ApiYanit<T> = ApiBasarili<T> | ApiHata;

function basariliMi<T>(yanit: ApiYanit<T>): yanit is ApiBasarili<T> {
  return yanit.basarili === true;
}

Assertion Functions

// asserts keyword ile assertion function
function stringOlmalidir(deger: unknown): asserts deger is string {
  if (typeof deger !== "string") {
    throw new Error(`String beklendi, ${typeof deger} alındı`);
  }
}

function islemYap(girdi: unknown): void {
  stringOlmalidir(girdi);
  // Bu noktadan sonra girdi kesinlikle string
  console.log(girdi.toUpperCase());
}

// Non-null assertion function
function doluOlmalidir<T>(
  deger: T | null | undefined,
  mesaj?: string
): asserts deger is T {
  if (deger === null || deger === undefined) {
    throw new Error(mesaj ?? "Değer null veya undefined olamaz");
  }
}

function kullaniciBul(id: number): string | null {
  return id === 1 ? "Tarik" : null;
}

const kullanici = kullaniciBul(1);
doluOlmalidir(kullanici, "Kullanıcı bulunamadı");
console.log(kullanici.toUpperCase()); // Güvenli!
Uyarı: Assertion function'lar, yanlış kontrol yapılırsa tip güvenliğini kırabilir. Assertion'ın gerçekten doğru kontrolü yaptığından emin olun. TypeScript derleme zamanında assertion'ın doğruluğunu kontrol edemez.

Kontrol Akışı ile Daraltma

// Truthiness narrowing
function isimYazdir(isim: string | null | undefined): void {
  if (isim) {
    // string (boş string hariç!)
    console.log(isim.toUpperCase());
  }
}

// Equality narrowing
function karsilastir(x: string | number, y: string | boolean): void {
  if (x === y) {
    // x ve y sadece string olabilir (ortak tip)
    console.log(x.toUpperCase());
  }
}

// Assignment narrowing
let deger: string | number;
deger = "merhaba";
console.log(deger.toUpperCase()); // string
deger = 42;
console.log(deger.toFixed(2)); // number

// Never tipine daraltma
type Renk = "kirmizi" | "yesil" | "mavi";

function renkKodu(renk: Renk): string {
  switch (renk) {
    case "kirmizi": return "#FF0000";
    case "yesil": return "#00FF00";
    case "mavi": return "#0000FF";
    default:
      const _kapsamliKontrol: never = renk;
      throw new Error(`Bilinmeyen renk: ${_kapsamliKontrol}`);
  }
}
Bilgi: TypeScript'in kontrol akışı analizi (Control Flow Analysis), if/else, switch/case, ternary operatör ve hatta mantıksal operatörler (&&, ||, ??) ile çalışır. Bu sayede çoğu durumda explicit type guard yazmaya gerek kalmaz.

Sonuç

Type narrowing, TypeScript'in tip güvenliğinin kalbidir. typeof, instanceof, in operatörü, custom type guard'lar ve assertion function'lar ile geniş tipleri güvenle daraltabilirsiniz. Discriminated union'lar ve exhaustive check pattern'leri ile birleştirildiğinde, çalışma zamanı hatalarını neredeyse tamamen ortadan kaldırabilirsiniz.

Kaynaklar

İlgili Yazılar