İçeriğe geç
Yazılım ve Teknoloji

Union, Generic Ve Utility Type’larla Daha Sağlam Kod Yazmak

Rıfat Akarca 👁️ 3 okunma 📅 ⏱️ 14 dk okuma
İçindekiler

TypeScript’in sağladığı tip güvenliğinden yararlanmak için her nesneye bir interface tanımlamak tek başına yeterli değildir. Gerçek projelerde bir değer birden fazla biçimde gelebilir, aynı fonksiyon farklı veri tipleriyle çalışabilir veya mevcut bir tipin yalnızca belirli alanlarına ihtiyaç duyulabilir.

Bu gibi durumlarda tipleri kopyalamak, bütün alanları opsiyonel yapmak veya any kullanmak kodun güvenilirliğini azaltır. TypeScript’in union, generic ve utility type özellikleri; gerçek iş kurallarını daha doğru modellemeyi, tekrarları azaltmayı ve hataları geliştirme aşamasında yakalamayı sağlar.

Bu rehberde union, generic ve utility type yapılarını ayrı ayrı inceleyecek, ardından gerçek projelerde nasıl birlikte kullanılabileceklerini örneklerle göstereceğiz.

Union Type Nedir?

Union type, bir değişkenin birden fazla olası tipten birini alabileceğini belirtir. Tipler dikey çizgi, yani | karakteriyle birleştirilir.

let kullaniciId: string | number;

kullaniciId = 42;
kullaniciId = "usr_42";

Bu değişkene yalnızca string veya number atanabilir:

kullaniciId = true;
// Hata: boolean atanamaz.

Union type, bir değerin olası biçimlerini any kullanmadan ifade etmemizi sağlar.

Union Type Neden any Kullanımından Daha Güvenlidir?

Aşağıdaki fonksiyon any kullandığı için bütün değerleri kabul eder:

function kimlikYazdir(id: any) {
  console.log(id.toUpperCase());
}

kimlikYazdir(42);

Kod derlenir ancak çalışma zamanında hata oluşur. Union type ile yalnızca desteklenen tipleri kabul edebilir ve her durumu güvenli biçimde işleyebiliriz:

function kimlikYazdir(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();
  }

  return id.toString();
}

TypeScript, typeof kontrolünden sonra ilgili bloktaki tipi otomatik olarak daraltır.

Literal Union ile Seçenekleri Sınırlandırmak

Union yalnızca string ve number gibi temel tiplerle kullanılmaz. Belirli değerlerden oluşan seçenek listeleri de oluşturulabilir.

type KullaniciRolu = "admin" | "editor" | "viewer";

function yetkiKontrol(rol: KullaniciRolu) {
  if (rol === "admin") {
    return "Tam yetki";
  }

  return "Sınırlı yetki";
}

Fonksiyon yalnızca tanımlanan üç rolü kabul eder:

yetkiKontrol("admin");
yetkiKontrol("editor");

yetkiKontrol("super-admin");
// Hata: Geçerli bir KullaniciRolu değil.

Literal union; durum, rol, tema, ödeme yöntemi ve sipariş aşaması gibi sınırlı seçenekleri modellemek için kullanılabilir.

type SiparisDurumu =
  | "bekliyor"
  | "hazirlaniyor"
  | "kargoda"
  | "teslim-edildi"
  | "iptal";

Bu yaklaşım, yazım hatalarını ve sistemin desteklemediği değerlerin kullanılmasını önler.

Discriminated Union ile Durum Modellemek

Gerçek uygulamalarda bir nesnenin alanları mevcut durumuna göre değişebilir. Örneğin bir API isteği yükleniyor, başarılı veya hatalı olabilir.

Zayıf bir model şöyle görünebilir:

interface ApiDurumu<T> {
  status: string;
  data?: T;
  error?: string;
}

Bu model aşağıdaki mantıksız nesneye izin verebilir:

const durum: ApiDurumu<string> = {
  status: "success",
  error: "Bir hata oluştu",
};

Başarılı bir durumda veri bulunmazken hata mesajı bulunması geçerli kabul edilmiştir.

Discriminated union kullanarak her durumun gerektirdiği alanları açıkça tanımlayabiliriz:

type ApiDurumu<T> =
  | { durum: "bekliyor" }
  | { durum: "yukleniyor" }
  | { durum: "basarili"; veri: T }
  | { durum: "hatali"; hata: string };

Kullanım sırasında durum alanı kontrol edildiğinde TypeScript nesnenin geri kalan yapısını bilir:

function sonucuGoster(durum: ApiDurumu<string>): string {
  switch (durum.durum) {
    case "bekliyor":
      return "İstek henüz başlamadı.";

    case "yukleniyor":
      return "Yükleniyor...";

    case "basarili":
      return durum.veri;

    case "hatali":
      return durum.hata;
  }
}

Bu yöntem, mümkün olmayan durumların daha en başta oluşturulmasını engeller.

Union Type Kullanırken Exhaustive Check

Union yapısına yeni bir seçenek eklendiğinde bütün kontrollerin güncellenmesi gerekir. never tipiyle eksik durumları derleme aşamasında yakalayabiliriz.

type OdemeDurumu =
  | { tip: "bekliyor" }
  | { tip: "tamamlandi"; islemId: string }
  | { tip: "reddedildi"; neden: string };

function assertNever(deger: never): never {
  throw new Error(`Beklenmeyen değer: ${JSON.stringify(deger)}`);
}

function odemeMesaji(durum: OdemeDurumu): string {
  switch (durum.tip) {
    case "bekliyor":
      return "Ödeme bekliyor.";

    case "tamamlandi":
      return `İşlem: ${durum.islemId}`;

    case "reddedildi":
      return durum.neden;

    default:
      return assertNever(durum);
  }
}

OdemeDurumu içine yeni bir seçenek eklenir ancak switch güncellenmezse TypeScript assertNever satırında hata üretir.

Generic Nedir?

Generic, bir fonksiyonun, tipin veya sınıfın farklı veri tipleriyle çalışmasını sağlarken tip bilgisini korur.

Aşağıdaki fonksiyon any kullandığı için giriş ve çıkış arasındaki ilişkiyi kaybeder:

function ilkEleman(liste: any[]): any {
  return liste[0];
}

const sonuc = ilkEleman(["TypeScript", "Node.js"]);

sonuc değerinin tipi any olur. Generic kullanarak dizinin eleman tipini dönüş değerine taşıyabiliriz:

function ilkEleman<T>(liste: T[]): T | undefined {
  return liste[0];
}

const teknoloji = ilkEleman(["TypeScript", "Node.js"]);
// string | undefined

const puan = ilkEleman([90, 85, 100]);
// number | undefined

Buradaki T, fonksiyon çağrılırken belirlenen tipi temsil eder. TypeScript çoğu zaman bu tipi parametreden otomatik olarak çıkarır.

Generic Neden any Değildir?

any, tip bilgisini ortadan kaldırır. Generic ise tipler arasındaki ilişkiyi korur.

function aynisiniDondur<T>(deger: T): T {
  return deger;
}

const metin = aynisiniDondur("Merhaba");
// string

const sayi = aynisiniDondur(42);
// number

Fonksiyon farklı tiplerle çalışabilir ancak girişte aldığı tipi dönüş değerinde korur.

Karşılaştırma:

function anyIleDondur(deger: any): any {
  return deger;
}

const sonuc = anyIleDondur("Merhaba");
// any

Generic esneklik sağlar; any ise kontrolü kaldırır.

Birden Fazla Generic Parametre Kullanmak

Bir fonksiyon birden fazla tip arasındaki ilişkiyi koruyabilir:

function eslestir<A, B>(birinci: A, ikinci: B): [A, B] {
  return [birinci, ikinci];
}

const sonuc = eslestir("ürün", 42);
// [string, number]

Nesne birleştirme örneği:

function birlestir<A extends object, B extends object>(
  birinci: A,
  ikinci: B,
): A & B {
  return {
    ...birinci,
    ...ikinci,
  };
}

const kullanici = birlestir(
  { id: 1, ad: "Rıfat" },
  { aktif: true },
);

console.log(kullanici.ad);
console.log(kullanici.aktif);

Dönüş tipi her iki nesnenin alanlarını içerir.

Generic Constraint Nedir?

Generic bir yapı her tipi kabul ettiğinde, tip üzerinde hangi işlemlerin yapılabileceği sınırlıdır.

function uzunluguYaz<T>(deger: T) {
  return deger.length;
  // Hata: T tipinde length bulunduğu garanti değil.
}

T tipini belirli bir yapıyla sınırlandırabiliriz:

interface UzunlukSahibi {
  length: number;
}

function uzunluguYaz<T extends UzunlukSahibi>(
  deger: T,
): number {
  return deger.length;
}

uzunluguYaz("TypeScript");
uzunluguYaz([1, 2, 3]);
uzunluguYaz({ length: 10 });

uzunluguYaz(42);
// Hata: number tipinde length yok.

Constraint, generic yapının esnekliğini korurken kullanılabilen tipleri sınırlar.

keyof ile Güvenli Özellik Erişimi

Bir nesnenin alan adına göre değer okumak istediğimizde string kullanmak güvenli değildir:

function alanOku(nesne: object, anahtar: string) {
  return nesne[anahtar];
}

keyof ve generic kullanarak anahtarın gerçekten nesneye ait olmasını sağlayabiliriz:

function alanOku<T, K extends keyof T>(
  nesne: T,
  anahtar: K,
): T[K] {
  return nesne[anahtar];
}

const kullanici = {
  id: 1,
  ad: "Rıfat",
  aktif: true,
};

const ad = alanOku(kullanici, "ad");
// string

const aktif = alanOku(kullanici, "aktif");
// boolean

alanOku(kullanici, "email");
// Hata: email nesnede bulunmuyor.

T[K], seçilen anahtara karşılık gelen alanın tipini döndürür.

Generic Interface Kullanımı

API yanıtları, sayfalama yapıları ve ortak bileşenler generic interface ile modellenebilir.

interface ApiYaniti<T> {
  basarili: boolean;
  veri: T;
  mesaj?: string;
}

interface Kullanici {
  id: number;
  ad: string;
}

const yanit: ApiYaniti<Kullanici> = {
  basarili: true,
  veri: {
    id: 1,
    ad: "Rıfat",
  },
};

Liste yanıtı için:

interface SayfalanmisYanit<T> {
  kayitlar: T[];
  toplam: number;
  sayfa: number;
  sayfaBoyutu: number;
}

const kullanicilar: SayfalanmisYanit<Kullanici> = {
  kayitlar: [
    {
      id: 1,
      ad: "Rıfat",
    },
  ],
  toplam: 1,
  sayfa: 1,
  sayfaBoyutu: 20,
};

Aynı sayfalama yapısı ürün, sipariş veya makale gibi farklı veri tipleriyle tekrar kullanılabilir.

Generic Varsayılan Tipler

Generic parametrelere varsayılan tip verilebilir:

interface ApiCevabi<T = unknown> {
  veri: T;
  durumKodu: number;
}

const genelCevap: ApiCevabi = {
  veri: {
    mesaj: "Tamam",
  },
  durumKodu: 200,
};

const metinCevabi: ApiCevabi<string> = {
  veri: "Tamam",
  durumKodu: 200,
};

Tip belirtilmediğinde unknown kullanılması, any kullanımına göre daha güvenli bir varsayılandır.

Utility Type Nedir?

Utility type’lar, mevcut tiplerden yeni tipler üretmek için TypeScript tarafından sunulan hazır yardımcı tiplerdir.

Örneğin bir Kullanici tipi üzerinden:

  • Bütün alanları opsiyonel yapabilir,
  • Yalnızca belirli alanları seçebilir,
  • Bazı alanları çıkarabilir,
  • Alanları salt okunur hâle getirebilir,
  • Fonksiyon dönüş tipini elde edebiliriz.

Bu sayede aynı veri yapısını küçük değişikliklerle tekrar tekrar yazmamız gerekmez.

Partial<T>: Bütün Alanları Opsiyonel Yapmak

Partial<T>, verilen tipteki bütün alanları opsiyonel hâle getirir.

interface Kullanici {
  id: number;
  ad: string;
  email: string;
  aktif: boolean;
}

function kullaniciGuncelle(
  id: number,
  degisiklikler: Partial<Kullanici>,
) {
  // Güncelleme işlemi
}

kullaniciGuncelle(1, {
  ad: "Rıfat Akarca",
});

kullaniciGuncelle(1, {
  email: "yeni@example.com",
  aktif: false,
});

Güncelleme fonksiyonunda bütün alanların gönderilmesi gerekmediği için Partial uygundur.

Ancak Partial<Kullanici>, id alanını da opsiyonel yapar. Güncellenmemesi gereken alanları önce çıkarmak daha doğru olabilir:

type KullaniciGuncelleme = Partial<
  Omit<Kullanici, "id">
>;

Required<T>: Bütün Alanları Zorunlu Yapmak

Required<T>, opsiyonel alanları zorunlu hâle getirir.

interface UygulamaAyarlari {
  tema?: "acik" | "koyu";
  dil?: "tr" | "en";
  bildirimler?: boolean;
}

type TamAyarlar = Required<UygulamaAyarlari>;

const ayarlar: TamAyarlar = {
  tema: "koyu",
  dil: "tr",
  bildirimler: true,
};

Yapılandırma değerleri ilk aşamada opsiyonel alınabilir, varsayılanlarla birleştirildikten sonra tam ve zorunlu bir tipe dönüştürülebilir.

Pick<T, K>: Belirli Alanları Seçmek

Pick, mevcut bir tipten yalnızca seçilen alanları alır.

interface Kullanici {
  id: number;
  ad: string;
  email: string;
  parolaHash: string;
  aktif: boolean;
}

type KullaniciOzeti = Pick<
  Kullanici,
  "id" | "ad" | "aktif"
>;

const ozet: KullaniciOzeti = {
  id: 1,
  ad: "Rıfat",
  aktif: true,
};

Liste ekranında bütün kullanıcı verisine ihtiyaç yoksa daha küçük ve amaca uygun bir tip oluşturulabilir.

Omit<T, K>: Belirli Alanları Çıkarmak

Omit, mevcut tipten belirtilen alanları kaldırır.

type GuvenliKullanici = Omit<
  Kullanici,
  "parolaHash"
>;

function kullaniciyiGonder(
  kullanici: GuvenliKullanici,
) {
  return kullanici;
}

Bu yaklaşım, hassas alanların API cevabına yanlışlıkla eklenmesini önlemeye yardımcı olur.

Yeni kayıt oluştururken sistem tarafından üretilen alanlar çıkarılabilir:

interface Makale {
  id: string;
  baslik: string;
  icerik: string;
  olusturulmaTarihi: Date;
}

type MakaleOlustur = Omit<
  Makale,
  "id" | "olusturulmaTarihi"
>;

Readonly<T>: Alanları Değiştirilemez Yapmak

Readonly<T>, tipin bütün alanlarını salt okunur hâle getirir.

interface SistemAyarlari {
  apiUrl: string;
  timeout: number;
}

const ayarlar: Readonly<SistemAyarlari> = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

ayarlar.timeout = 10000;
// Hata: Salt okunur alan değiştirilemez.

Bu kontrol derleme zamanında geçerlidir. Çalışma zamanında gerçek değişmezlik gerekiyorsa ayrıca uygun JavaScript yöntemleri kullanılmalıdır.

Record<K, T>: Anahtar-Değer Yapısı Oluşturmak

Record, belirli anahtarların aynı türde değerlerle eşleştirildiği nesne tipi oluşturur.

type Rol = "admin" | "editor" | "viewer";

const rolEtiketleri: Record<Rol, string> = {
  admin: "Yönetici",
  editor: "Editör",
  viewer: "Görüntüleyici",
};

Union içine yeni bir rol eklendiğinde TypeScript, rolEtiketleri nesnesinin de güncellenmesini ister.

Nesne değerleri daha karmaşık olabilir:

interface RolAyari {
  duzenleyebilir: boolean;
  silebilir: boolean;
}

const rolAyarlari: Record<Rol, RolAyari> = {
  admin: {
    duzenleyebilir: true,
    silebilir: true,
  },
  editor: {
    duzenleyebilir: true,
    silebilir: false,
  },
  viewer: {
    duzenleyebilir: false,
    silebilir: false,
  },
};

Exclude<T, U>: Union İçinden Tip Çıkarmak

Exclude, bir union içinden belirtilen seçenekleri kaldırır.

type Durum =
  | "taslak"
  | "yayinda"
  | "arsivlendi"
  | "silindi";

type AktifDurum = Exclude<
  Durum,
  "silindi"
>;
// "taslak" | "yayinda" | "arsivlendi"

İşlem bağlamında kullanılmaması gereken seçenekler bu şekilde çıkarılabilir.

Extract<T, U>: Ortak Tipleri Seçmek

Extract, iki tip arasındaki ortak seçenekleri alır.

type TumOlaylar =
  | "click"
  | "focus"
  | "submit"
  | "error";

type FormOlaylari = Extract<
  TumOlaylar,
  "focus" | "submit" | "change"
>;
// "focus" | "submit"

NonNullable<T>: null ve undefined Değerlerini Çıkarmak

type ApiDegeri =
  | string
  | null
  | undefined;

type GecerliDeger = NonNullable<ApiDegeri>;
// string

Bir değer doğrulandıktan sonraki durumu modellemek için kullanılabilir.

function degerVarMi<T>(
  deger: T,
): deger is NonNullable<T> {
  return deger !== null && deger !== undefined;
}

const degerler = [
  "TypeScript",
  null,
  "Node.js",
  undefined,
];

const gecerliDegerler = degerler.filter(degerVarMi);
// string[]

ReturnType<T>: Fonksiyonun Dönüş Tipini Almak

Bir fonksiyonun dönüş tipini tekrar yazmak yerine ReturnType kullanılabilir.

function kullaniciOlustur() {
  return {
    id: 1,
    ad: "Rıfat",
    aktif: true,
  };
}

type Kullanici = ReturnType<
  typeof kullaniciOlustur
>;

Fonksiyonun dönüş yapısı değiştiğinde tip de otomatik olarak güncellenir.

Ancak iş modelinin temel tipi yalnızca bir fonksiyonun mevcut uygulamasına bağlıysa bu yapı gereksiz bağımlılık oluşturabilir. ReturnType, özellikle yardımcı ve fabrika fonksiyonlarında değerlidir.

Parameters<T>: Fonksiyon Parametrelerini Almak

Parameters, bir fonksiyonun parametrelerini tuple olarak verir.

function kullaniciKaydet(
  ad: string,
  yas: number,
  aktif: boolean,
) {
  // Kayıt işlemi
}

type KayitParametreleri = Parameters<
  typeof kullaniciKaydet
>;
// [ad: string, yas: number, aktif: boolean]

Bir fonksiyonu saran yardımcı fonksiyonlarda parametre tiplerini tekrar etmeden korumak için kullanılabilir.

Awaited<T>: Promise Sonucunu Elde Etmek

Awaited, Promise çözüldüğünde elde edilen tipi verir.

async function kullaniciGetir() {
  return {
    id: 1,
    ad: "Rıfat",
  };
}

type Kullanici = Awaited<
  ReturnType<typeof kullaniciGetir>
>;
// { id: number; ad: string }

Asenkron fonksiyonların gerçek veri tipini elde etmek için ReturnType ile birlikte kullanılabilir.

Utility Type’lar Birlikte Nasıl Kullanılır?

Utility type’ların gücü, birlikte kullanıldıklarında daha belirgin hâle gelir.

Örnek bir kullanıcı modeli:

interface Kullanici {
  id: string;
  ad: string;
  email: string;
  rol: "admin" | "editor" | "viewer";
  parolaHash: string;
  olusturulmaTarihi: Date;
  guncellenmeTarihi: Date;
}

Yeni kullanıcı oluşturma tipi:

type YeniKullanici = Omit<
  Kullanici,
  | "id"
  | "parolaHash"
  | "olusturulmaTarihi"
  | "guncellenmeTarihi"
>;

Kullanıcı güncelleme tipi:

type KullaniciGuncelleme = Partial<
  Pick<Kullanici, "ad" | "email" | "rol">
>;

Dışarıya gönderilecek güvenli kullanıcı tipi:

type GuvenliKullanici = Omit<
  Kullanici,
  "parolaHash"
>;

Liste görünümü:

type KullaniciListeOgesi = Pick<
  Kullanici,
  "id" | "ad" | "email" | "rol"
>;

Bu tiplerin tamamı ana Kullanici modelinden türetilir. Ana model değiştiğinde ilişkili tiplerin uyumluluğu TypeScript tarafından kontrol edilir.

Union, Generic ve Utility Type Birlikte Kullanımı

Şimdi üç yaklaşımı tek bir API modelinde birleştirelim.

type ApiSonucu<T> =
  | {
      durum: "basarili";
      veri: T;
    }
  | {
      durum: "hatali";
      hata: {
        kod: string;
        mesaj: string;
      };
    };

interface Urun {
  id: string;
  ad: string;
  fiyat: number;
  stok: number;
  olusturulmaTarihi: Date;
}

type UrunOlustur = Omit<
  Urun,
  "id" | "olusturulmaTarihi"
>;

type UrunGuncelle = Partial<UrunOlustur>;

async function urunOlustur(
  veri: UrunOlustur,
): Promise<ApiSonucu<Urun>> {
  try {
    const urun: Urun = {
      id: crypto.randomUUID(),
      olusturulmaTarihi: new Date(),
      ...veri,
    };

    return {
      durum: "basarili",
      veri: urun,
    };
  } catch {
    return {
      durum: "hatali",
      hata: {
        kod: "URUN_OLUSTURULAMADI",
        mesaj: "Ürün oluşturulamadı.",
      },
    };
  }
}

Bu örnekte:

  • Union type başarılı ve hatalı sonucu ayırır.
  • Generic yapı aynı sonuç modelinin farklı verilerle kullanılmasını sağlar.
  • Omit sistem tarafından oluşturulan alanları giriş tipinden çıkarır.
  • Partial güncellemede alanların isteğe bağlı gönderilmesini sağlar.

Sonuç iş kurallarını daha açık ifade eden ve yanlış durumları sınırlandıran bir tip modelidir.

Form Durumunu Sağlam Biçimde Modellemek

Formlar, union ve utility type kullanımının faydalı olduğu alanlardan biridir.

interface KayitFormu {
  ad: string;
  email: string;
  parola: string;
}

type FormHatalari<T> = Partial<
  Record<keyof T, string>
>;

type FormDurumu<T> =
  | {
      durum: "duzenleniyor";
      degerler: T;
      hatalar: FormHatalari<T>;
    }
  | {
      durum: "gonderiliyor";
      degerler: T;
    }
  | {
      durum: "basarili";
      sonucId: string;
    }
  | {
      durum: "hatali";
      degerler: T;
      genelHata: string;
    };

Kullanım:

const durum: FormDurumu<KayitFormu> = {
  durum: "duzenleniyor",
  degerler: {
    ad: "",
    email: "",
    parola: "",
  },
  hatalar: {
    email: "Geçerli bir e-posta girin.",
  },
};

FormHatalari<T> yalnızca formda gerçekten bulunan alan adlarını kabul eder. Yanlış bir alan adı yazıldığında TypeScript hata verir.

Tipleri Gereğinden Fazla Karmaşıklaştırmayın

TypeScript güçlü tip araçları sunar ancak her yapının mümkün olan en soyut hâle getirilmesi gerekmez.

Aşağıdaki durumlarda daha basit bir tip daha iyi olabilir:

  • Tip yalnızca bir yerde kullanılıyorsa
  • Generic yapı okunabilirliği belirgin biçimde azaltıyorsa
  • Utility type zinciri ana modeli anlamayı zorlaştırıyorsa
  • İş kuralı doğrudan bir interface ile daha açık ifade edilebiliyorsa
  • Tip sistemi çalışma zamanı doğrulamasının yerine kullanılmaya çalışılıyorsa

Amaç en kısa veya en “zekice” tipi yazmak değil, hatalı durumları önleyen ve ekip tarafından kolayca anlaşılabilen modeller oluşturmaktır.

Sık Yapılan Hatalar

Her yerde geniş union kullanmak

type Deger =
  | string
  | number
  | boolean
  | object
  | null;

Bu yapı çoğu durumda any kullanımına yakın bir belirsizlik oluşturur. Yalnızca iş kuralında gerçekten geçerli olan seçenekler tanımlanmalıdır.

Generic kullanıp tip ilişkisi kurmamak

function isle<T>(deger: T): string {
  return String(deger);
}

Burada dönüş tipi her zaman string olduğu için generic gerekli olmayabilir:

function isle(deger: unknown): string {
  return String(deger);
}

Generic, tip parametresi giriş, çıkış veya birden fazla alan arasında anlamlı ilişki kuruyorsa kullanılmalıdır.

Her güncellemede doğrudan Partial<Model> kullanmak

Partial<Kullanici> sistem tarafından yönetilen id gibi alanları da opsiyonel ve güncellenebilir gösterir. Önce izin verilen alanları Pick ile seçmek daha güvenlidir.

type KullaniciGuncelleme = Partial<
  Pick<Kullanici, "ad" | "email">
>;

Tipleri çalışma zamanı doğrulaması sanmak

const veri = await yanit.json() as Kullanici;

Type assertion gelen veriyi doğrulamaz. Dış veri çalışma zamanında kontrol edilmelidir.

Çok fazla tip iddiası kullanmak

Sık sık as kullanma ihtiyacı, modelin gerçek veriyi doğru yansıtmadığını gösterebilir. Öncelikle union, type guard ve generic constraint seçenekleri değerlendirilmelidir.

Readonly ile tam değişmezlik beklemek

Readonly<T> yalnızca üst seviyedeki alanları salt okunur yapar ve derleme zamanında kontrol sağlar. İç içe nesneler için ayrıca derin readonly modeli gerekebilir.

Hangi Yapıyı Ne Zaman Kullanmalıyım?

İhtiyaçKullanılabilecek Yapı
Bir değer birkaç farklı tip alabilirUnion type
Seçenekleri belirli değerlerle sınırlandırmakLiteral union
Duruma göre nesne yapısı değişiyorDiscriminated union
Aynı kod farklı tiplerle çalışıyorGeneric
Generic tipi belirli yapılarla sınırlandırmakGeneric constraint
Nesnenin güvenli anahtarlarını kullanmakkeyof
Bütün alanları opsiyonel yapmakPartial
Bütün alanları zorunlu yapmakRequired
Belirli alanları seçmekPick
Belirli alanları çıkarmakOmit
Alanları salt okunur yapmakReadonly
Anahtar-değer haritası oluşturmakRecord
Union içinden seçenek çıkarmakExclude
null ve undefined değerlerini çıkarmakNonNullable
Fonksiyonun dönüş tipini almakReturnType
Asenkron sonucu elde etmekAwaited

Daha Sağlam Tipler İçin Pratik İlkeler

İş kurallarını tiplerle ifade edin

Bir sipariş hem “iptal” hem “teslim edildi” durumunda olamıyorsa tip modeli de buna izin vermemelidir.

İmkânsız durumları modellenemez hâle getirin

Çok sayıda opsiyonel alana sahip tek bir interface yerine discriminated union kullanmayı değerlendirin.

Tip bilgisini koruyun

Giriş ve çıkış arasında ilişki varsa any yerine generic kullanın.

Ana tipleri kopyalamayın

Yeni tipler oluştururken uygun durumlarda Pick, Omit ve Partial kullanın.

Dış veriyi doğrulayın

TypeScript tipleri çalışma zamanında bulunmaz. API, dosya ve kullanıcı girdileri uygulamaya alınmadan önce doğrulanmalıdır.

Okunabilirliği önceliklendirin

Karmaşık tipleri anlamlı isimlere ayırın:

type DuzenlenebilirAlanlar = Pick<
  Kullanici,
  "ad" | "email"
>;

type KullaniciGuncelleme =
  Partial<DuzenlenebilirAlanlar>;

Bu kullanım, uzun bir utility type zincirini her yerde tekrar etmekten daha anlaşılırdır.

Sonuç

Union, generic ve utility type yapıları TypeScript’in en güçlü araçları arasındadır. Doğru kullanıldıklarında yalnızca kod tamamlama sağlamaz; iş kurallarını doğrudan tip sistemine taşır ve hatalı durumların oluşmasını zorlaştırır.

Temel yaklaşımı şöyle özetleyebiliriz:

  • Birden fazla geçerli seçenek için union type kullanın.
  • Duruma göre değişen nesneleri discriminated union ile modelleyin.
  • Tip bilgisini kaybetmeden tekrar kullanılabilir kod yazmak için generic kullanın.
  • Generic yapıların sınırlarını constraint ile belirleyin.
  • Mevcut tiplerden yeni modeller üretmek için utility type’lardan yararlanın.
  • Dış verileri çalışma zamanında doğrulamayı unutmayın.
  • Tipleri gereksiz yere karmaşıklaştırmadan gerçek iş kurallarını yansıtın.

Sağlam TypeScript kodu, her değişkene mümkün olan en ayrıntılı tipi yazmak değildir. Önemli olan geçerli durumları açık, geçersiz durumları ise mümkün olduğunca oluşturulamaz hâle getirmektir.

Sıkça Sorulan Sorular

TypeScript union type nedir?

Union type, bir değişkenin tanımlanan birden fazla tipten birini alabilmesini sağlar. Tipler | karakteriyle birleştirilir.

Generic ile any arasındaki fark nedir?

any tip bilgisini ve kontrolleri ortadan kaldırır. Generic ise farklı tiplerle çalışırken giriş ve çıkış arasındaki tip ilişkisini korur.

Utility type nedir?

Utility type, mevcut bir TypeScript tipinden yeni tipler oluşturmayı sağlayan hazır yardımcı yapılardır. Partial, Pick, Omit, Readonly ve Record sık kullanılan örneklerdir.

Partial ve Pick arasındaki fark nedir?

Partial, bir tipteki bütün alanları opsiyonel yapar. Pick ise yalnızca belirtilen alanları seçerek yeni bir tip oluşturur.

Omit ne zaman kullanılmalıdır?

Bir modeldeki bazı alanların yeni tipte bulunmaması gerektiğinde kullanılır. Örneğin parola bilgisini API yanıtından veya sistem tarafından oluşturulan id alanını kayıt girdisinden çıkarmak için tercih edilebilir.

Generic her fonksiyonda kullanılmalı mı?

Hayır. Generic, tip parametresi giriş ve çıkış arasında anlamlı bir ilişki kuruyorsa kullanılmalıdır. Sabit bir tip yeterliyse daha basit tanım tercih edilmelidir.