TypeScript'te union tipi (A | B) bir değerin birkaç tipten herhangi biri olabileceğini, intersection tipi (A & B) ise değerin birkaç tipin hepsini aynı anda karşılaması gerektiğini söyler. Union ile çalışırken yalnızca tüm üyelerde geçerli olan işlemleri yapabilirsiniz; daha fazlası için tipi daraltırsınız. Ortak bir literal alanla (kind, type, ok gibi) ayırt edilen union'lara discriminated union denir ve TypeScript bu alanı kontrol ettiğinizde doğru üyeyi kendisi seçer.

Bu yazıda union ve intersection'ı tanımlamaktan başlayıp, birlikte kullanıldıklarında ortaya çıkan tuzaklara ve discriminated union tasarımına kadar ilerliyoruz. Örnekler TypeScript 6.0.3 ile tsc --strict --target es2022 --moduleDetection force seçenekleriyle derlendi; hata kodları ve mesajları bu derleyicinin gerçek çıktısıdır. Daraltma tekniklerinin tamamı için narrowing ve type guard yazısına bakabilirsiniz.

Union tipi: birkaç tipten biri

Union tipi: birkaç tipten biri

Union, iki ya da daha fazla tipin arasına | koyarak yazılır ve her birine union'ın üyesi denir. Aşağıdaki fonksiyon hem sayı hem metin kimlik kabul eder, ama nesne kabul etmez:

typescript
function kimlikYaz(id: number | string) {
  console.log("Kimlik: " + id);
}

kimlikYaz(101); // geçerli
kimlikYaz("202"); // geçerli
kimlikYaz({ id: 22342 });
// TS2345: Argument of type '{ id: number; }' is not assignable to parameter of type 'string | number'.

Uzun union'ları alt alta yazarken ilk üyenin önüne de | koyabilirsiniz; bu yalnızca biçimle ilgilidir ve anlamı değiştirmez. Sık kullandığınız bir union'a type ile ad vermek okunabilirliği artırır; bunun neden interface ile yapılamadığını interface ve type farkı yazısında anlattık.

Union ile çalışmak: yalnızca ortak işlemler serbest

Union tipinde bir değer vermek kolaydır, üyelerden birine uyan herhangi bir değer yeterlidir. Asıl soru, elinizde union tipinde bir değer varken onunla ne yapabileceğinizdir. TypeScript yalnızca her üye için geçerli olan işlemlere izin verir:

typescript
function buyukYaz(id: number | string) {
  return id.toUpperCase();
}
// TS2339: Property 'toUpperCase' does not exist on type 'string | number'.

Çözüm, JavaScript'te zaten yazacağınız kontrolü yazmaktır. typeof id === "string" kontrolünden sonra derleyici o dalda id'nin string olduğunu bilir. Buna daraltma (narrowing) denir:

typescript
function buyukYaz(id: number | string) {
  if (typeof id === "string") {
    return id.toUpperCase(); // burada id: string
  }
  return id.toFixed(2); // burada id: number
}

console.log(buyukYaz("ab12")); // AB12
console.log(buyukYaz(7)); // 7.00

Bazen üyelerin hepsinde aynı adlı bir özellik ya da metot bulunur. O zaman daraltmaya gerek kalmaz. Hem dizilerin hem string'lerin slice metodu olduğu için aşağıdaki kod derlenir ve dönüş tipi number[] | string olarak çıkarılır:

typescript
function ilkUc(x: number[] | string) {
  return x.slice(0, 3);
}

console.log(ilkUc([5, 6, 7, 8])); // [ 5, 6, 7 ]
console.log(ilkUc("TypeScript")); // Typ

Intersection tipi: hepsi aynı anda

Intersection tipi: hepsi aynı anda

Intersection, & operatörüyle yazılır ve çoğunlukla var olan nesne tiplerini birleştirmek için kullanılır. Sonuç, tüm parçalardaki üyelerin hepsine sahip olan bir tiptir:

typescript
interface Renkli {
  renk: string;
}
interface Daire {
  yaricap: number;
}

function ciz(d: Renkli & Daire) {
  console.log(`${d.renk} daire, yarıçap ${d.yaricap}`);
}

ciz({ renk: "mavi", yaricap: 42 }); // mavi daire, yarıçap 42

Nesne literal'i doğrudan verildiğinde fazladan özellik denetimi de devreye girer. Yazım hatası yapılmış bir alan adı yakalanır ve derleyici doğru adı önerir:

typescript
interface Renkli {
  renk: string;
}
interface Daire {
  yaricap: number;
}
declare function ciz(d: Renkli & Daire): void;

ciz({ renk: "kırmızı", yaricapp: 42 });
// TS2561: Object literal may only specify known properties, but 'yaricapp' does not exist in type 'Renkli & Daire'. Did you mean to write 'yaricap'?

Eski handbook bu yapının tipik bir kullanımını gösterir: Tüm ağ yanıtlarında ortak olan hata alanlarını ayrı bir tipte tutup her yanıt tipine intersection ile eklemek. Böylece hata yapısını tek yerde tanımlamış olursunuz.

Çakışan alanlar ve never

Intersection'daki parçalarda aynı adlı bir alan farklı tiplerle geçiyorsa TypeScript bunu bildirim anında hata saymaz. Alanın iki tipi birden karşılaması gerekir; string ve number olan bir değer olmadığından sonuç never olur:

typescript
type A = { kod: string };
type B = { kod: number };
type AB = A & B;

const x: AB = { kod: "42" };
// TS2322: Type 'string' is not assignable to type 'never'.

type Imkansiz = string & number; // never

Interface'te extends kullansaydınız aynı çakışma bildirimin olduğu satırda hata verirdi. Bu yüzden intersection ile kurulan tiplerde beklenmedik bir never gördüğünüzde ilk bakacağınız yer çakışan alan adlarıdır.

Discriminated union: ortak bir etiketle ayırt etmek

Handbook'taki klasik örnekte daire ve kare tek bir tipte, isteğe bağlı alanlarla modellenir. Bu tasarım, kind kontrol edilse bile derleyiciye yarıçap alanının dolu olduğunu söylemez:

typescript
interface Sekil {
  kind: "daire" | "kare";
  yaricap?: number;
  kenar?: number;
}

function alan(s: Sekil) {
  if (s.kind === "daire") {
    return Math.PI * s.yaricap ** 2;
  }
}
// TS18048: 's.yaricap' is possibly 'undefined'.

Sorun tipin tasarımındadır: Derleyici kind ile yaricap arasında bir bağ olduğunu bilemez. Çözüm, her şekli kendi zorunlu alanlarıyla ayrı bir tip olarak yazıp, hepsinde ortak olan kind alanına farklı bir literal tip vermektir:

typescript
interface Daire {
  kind: "daire";
  yaricap: number;
}
interface Kare {
  kind: "kare";
  kenar: number;
}
type Sekil = Daire | Kare;

function alan(s: Sekil): number {
  switch (s.kind) {
    case "daire":
      return Math.PI * s.yaricap ** 2; // s: Daire
    case "kare":
      return s.kenar ** 2; // s: Kare
  }
}

console.log(alan({ kind: "kare", kenar: 3 })); // 9
console.log(alan({ kind: "daire", yaricap: 1 }).toFixed(4)); // 3.1416

Union'ın her üyesi literal tipte ortak bir alan taşıdığında TypeScript bunu discriminated union olarak görür ve o alana bakan her kontrolde union'ı uygun üyeye daraltır. Buradaki kind alanına ayırt edici (discriminant) özellik denir. Aynı yapı yanlış yazılmış etiketleri de yakalar; s.kind === "dikdortgen" gibi bir karşılaştırma TS2367 hatası verir, çünkü iki tipin hiç ortak değeri yoktur.

Etiketin string olması gerekmez. Boolean literal'ler de ayırt edici olabilir ve sonuç döndüren fonksiyonlarda kullanışlıdır:

typescript
type Sonuc =
  | { ok: true; deger: number }
  | { ok: false; hata: string };

function bol(a: number, b: number): Sonuc {
  if (b === 0) return { ok: false, hata: "sıfıra bölme" };
  return { ok: true, deger: a / b };
}

const r = bol(10, 0);
if (r.ok) {
  console.log(r.deger);
} else {
  console.log("Hata:", r.hata); // Hata: sıfıra bölme
}

TypeScript 4.4'ten beri kontrolü bir sabite taşımak da daraltmayı bozmaz. const daireMi = s.kind === "daire" yazıp if (daireMi) dediğinizde derleyici yine s'yi Daire'ye daraltır. Sürüm notlarına göre bu, kontrol edilen değer const, readonly bir özellik ya da değiştirilmemiş bir parametre olduğunda çalışır.

Tüm durumları ele aldınız mı?

Union'a yeni bir üye eklediğinizde onu işlemeyi unuttuğunuz yerleri bulmak istersiniz. En basit yol, fonksiyona açık bir dönüş tipi yazmaktır. switch tüm üyeleri kapsamıyorsa fonksiyonun bazen undefined döndürebileceği ortaya çıkar ve derleyici bunu bildirir:

typescript
type Durum =
  | { durum: "yukleniyor" }
  | { durum: "hata"; kod: number }
  | { durum: "basarili" };

function etiket(d: Durum): string {
  switch (d.durum) {
    case "yukleniyor":
      return "Yükleniyor";
    case "hata":
      return `Hata ${d.kod}`;
  }
}
// TS2366: Function lacks ending return statement and return type does not include 'undefined'.

Daha açık bir hata mesajı için never tipine dayanan kapsamlılık denetimi kullanılır; eksik üyenin adını doğrudan hata mesajında görürsünüz. Bu tekniği narrowing yazısındaki exhaustiveness bölümünde, never tipinin kendisini ise any, unknown ve never yazısında anlatıyoruz.

Union ve intersection karşılaştırması

Union ve intersection karşılaştırması: Anlamı, Değer verirken, Değeri kullanırken, Primitive örneği
KonuUnion (A | B)Intersection (A & B)
AnlamıA ya da BHem A hem B
Değer verirkenBir üyeye uymak yeterTüm parçalara uymak gerekir
Değeri kullanırkenYalnızca ortak üyeler, sonra daraltmaTüm parçaların üyeleri
Primitive örneğistring | number geniş bir tiptirstring & number never olur
Tipik kullanımSeçenekler, durum makineleri, sonuç tipleriNesne tiplerini birleştirmek
unknown ileunknown | T sonucu unknownunknown & T sonucu T

Tablonun son satırı TypeScript 3.0 sürüm notlarındaki kurallardan gelir: Union içinde unknown diğer üyeleri yutar, intersection içinde ise diğer tipler unknown'ı yutar.

Sık yapılan hatalar

  • Union'da daraltmadan üyeye özgü metot çağırmak: TS2339 alırsınız. Önce typeof, in ya da ayırt edici alanla tipi daraltın.
  • Discriminated union yerine isteğe bağlı alanlarla tek tip yazmak: Derleyici alanların ne zaman dolu olduğunu bilemez ve sizi ! kullanmaya iter. Her durumu ayrı tip olarak yazın.
  • Ayırt edici alanı string yapmak: Etiket alanı geniş string olursa daraltma çalışmaz; her üyede farklı bir literal tip olmalıdır.
  • Intersection'da çakışan alanları gözden kaçırmak: Alan sessizce never olur ve hata uzak bir yerde çıkar.
  • Overload'a gereğinden çok başvurmak: Aynı dönüş tipine sahip birkaç overload yerine union parametreli tek imza çoğu zaman daha kullanışlıdır; ayrıntısı fonksiyon tipleri yazısında.

Sık Sorulan Sorular

Union ile enum arasında nasıl seçim yaparım?

String literal union'lar çalışma zamanında hiçbir kod üretmez; yalnızca tip düzeyindedir. Enum ise bir değer de oluşturur. Seçenekleri enum ve alternatifleri yazısında karşılaştırıyoruz.

Discriminated union için alanın adı önemli mi?

Hayır. kind, type, status ya da ok olabilir. Önemli olan alanın tüm üyelerde bulunması ve her üyede farklı bir literal tipe sahip olmasıdır.

Intersection ile extends aynı şey mi?

Sonuç çoğu zaman benzerdir, ama çakışmalarda davranış farklıdır: extends hatayı tanımda verir, intersection alanı never yapar.

API yanıtlarını nasıl tiplemeliyim?

Başarılı ve hatalı yanıtları ayrı üyeler olarak, ortak bir ayırt edici alanla yazmak en okunaklı yoldur. Gelen veriyi gerçekten doğrulamak ise çalışma zamanı işidir; tip yalnızca derleme sırasında yardımcı olur.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: TypeScript öğrenme rehberi · Narrowing ve type guard · interface ve type farkı · Literal tipler ve as const · any, unknown, never ve void · TypeScript ileri seviye yazısı