Menu

TypeScript Discriminated Union: Etiketli Union'lar

Discriminated union, kind veya status gibi ortak bir literal etiket özelliği paylaşan nesne tiplerinin union'ıdır. Etiketi kontrol etmek nesnenin tamamını daraltır. Kalıbı, switch ile daraltmayı, never ile kapsamlı kontrolleri ve API sonuçlarını, istek durumunu ve durum makinelerini nasıl modelleyeceğinizi öğrenin.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Discriminated union (etiketli union, tagged union olarak da bilinir), hepsinin ortak bir özelliğe, yani etikete, her üyede farklı bir literal değerle sahip olduğu nesne tiplerinin bir union'ıdır. Etiketi kontrol etmek TypeScript'e hangi üyeye sahip olduğunuzu söyler ve o üyenin diğer özellikleri kullanılabilir hale gelir.

case "circle" içinde shape.width okumak bir derleme hatası olurdu, çünkü daire üyesinin width özelliği yoktur. Etiket sıradan bir veridir: çalışma zamanında sadece bir string özelliktir ve switch düz JavaScript'tir.

Bir Union'ı Discriminated Yapan Nedir

Etiket üzerinden daraltma, üç koşul sağlandığında çalışır:

  1. Her üye bir nesne tipidir.
  2. Her üye aynı özellik adına (ayırıcıya) sahiptir. Ad size kalmıştır: kind, type, status ve tag yaygındır.
  3. Her üyede bu özellik bir literal tipe sahiptir: bir string, number veya boolean literal'i ya da null/undefined.
type UiEvent =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "scroll"; delta: number };

type ApiResponse =
  | { ok: true; data: string }    // boolean literal tags
  | { ok: false; error: string };

type Message =
  | { version: 1; text: string }  // number literal tags
  | { version: 2; text: string; lang: string };

Bir üye etiketi literal yerine düz string olarak bildirirse etiketi karşılaştırmak artık union'ı daraltmaz ve üyeye özgü özellikler erişilemez kalır (TS2339).

Etiket Üzerinden Daraltmak

TypeScript'in etiket üzerinde anladığı her kontrol nesneyi daraltır: switch, if/else, === ve !==, hatta const olduğu sürece destructuring ile alınmış bir etiket:

function describe(e: UiEvent): string {
  if (e.type === "click") return `click at ${e.x},${e.y}`;

  const { type } = e;        // destructured tags narrow too
  if (type === "keypress") return `key ${e.key}`;

  return `scroll by ${e.delta}`; // only "scroll" is left
}

"radius" in shape ile bir özelliği kontrol etmek de daraltır, ama bir etiketi karşılaştırmak okunması daha kolaydır ve kapsamlı kontrolü mümkün kılar.

Kapsamlı Kontroller

Kalıbın en büyük faydası union büyüdüğünde görünür. Açık bir dönüş tipiyle ve default olmadan TypeScript, switch ifadesinin her etiketi kapsaması gerektiğini bilir; bu yüzden case'i olmayan yeni bir üye bir derleme hatasıdır:

index.ts(7,30): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.

Bu mesaj hangi case'in eksik olduğunu söylemez. assertNever yardımcısı söyler; ayrıca dışarıdan gelen veriler (JSON, eski bir istemci) tiplerin var olamaz dediği bir etiket taşırsa çalışma zamanında da hata fırlatır:

Case eklemeden beşinci bir status ekleyin, assertNever(state) satırı üyeyi adıyla belirterek Argument of type '{ status: "..."; ... }' is not assignable to parameter of type 'never' bildirir. Daha fazlası never sayfasında.

İmkansız Durumları İmkansız Kılmak

Yukarıdaki RequestState, yaygın ve daha zayıf bir biçimin yerini alır:

// Every combination is allowed, including nonsense
type LooseState<T> = {
  loading: boolean;
  data?: T;
  error?: string;
};

const nonsense: LooseState<string[]> = { loading: true, data: ["a"], error: "timeout" };

Opsiyonel alanlarla "veri ve hatayla birlikte yükleniyor" durumu tip kontrolünden geçer ve her okuyucu hangi kombinasyonların gerçekten olabileceğini tahmin etmek zorunda kalır. Discriminated union ile data yalnızca success durumunda, error da yalnızca error durumunda vardır; bu yüzden state.data okuyan kod önce success durumunda olduğunu kanıtlamalıdır. Tipin kendisi geçerli durumları belgeler.

Sonuçları Modellemek: Başarı ya da Başarısızlık

"Çalıştı ya da çalışmadı" için bir boolean etiket yeterlidir. Bu Result tipi, geçersiz girdi gibi beklenen başarısızlıklar için hata fırlatmaya yaygın bir alternatiftir:

Çağıran, önce result.ok kontrolünü yapmadan result.value okuyamaz; exception'larla ya da null dönüşleriyle unutulması kolay olan kontrol de tam olarak budur.

Durum Makineleri ve Reducer'lar

Biri durumlar, biri aksiyonlar için olmak üzere iki discriminated union bir durum makinesini tanımlar. Bir reducer aksiyon üzerinde switch yapar ve sonraki durumu döndürür; derleyici döndürülen her nesnenin geçerli bir durum olduğunu kontrol eder:

{ ...state, name: "paused" } yalnızca state önce playing üyesine daraltıldığı için derlenir, böylece kopya track ve position taşır. Tek başına { name: "paused" } döndürmek hata olurdu: duraklatılmış bir durum bir parça (track) gerektirir. Redux reducer'larının ve React'teki useReducer fonksiyonunun kullandığı biçim de budur.

Genişleme Tuzağı

Etiket bir literal tip olarak kalmalıdır. Notasyonsuz bir değişkende oluşturulan bir nesnenin etiketi string tipine genişletilir ve o zaman hiçbir üyeyle eşleşmez:

type Shape = { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };
declare function area(shape: Shape): number;

const c = { kind: "circle", radius: 2 }; // kind: string
area(c);
// error TS2345: Argument of type '{ kind: string; radius: number; }' is not assignable to parameter of type 'Shape'.

const ok1: Shape = { kind: "circle", radius: 2 };        // annotate the variable
const ok2 = { kind: "circle", radius: 2 } as const;      // or keep the literal with as const
area({ kind: "circle", radius: 2 });                     // or build it where Shape is expected

Temeldeki kural, yani değiştirilebilir özelliklerin genişlemesi, literal tipler sayfasında açıklanıyor.

Sıkça Sorulan Sorular

TypeScript'te discriminated union nedir?

Her üyesinin aynı özelliğe (ayırıcı ya da etiket) farklı bir literal tiple sahip olduğu nesne tiplerinin bir union'ıdır; örneğin { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number }. shape.kind === "circle" kontrolü shape değerini daire üyesine daraltır, böylece diğer özellikleri kullanılabilir hale gelir.

Union ile discriminated union arasındaki fark nedir?

Discriminated union, fazladan bir kurala sahip bir union'dır: tüm üyeler benzersiz bir literal tipe sahip bir özelliği paylaşır. Cat | Fish gibi düz bir union in veya özel type guard'larla daraltılmalıdır; bir discriminated union ise tek bir özellik karşılaştırılarak daraltılır ve o özellik üzerindeki bir switch kapsamlılık açısından kontrol edilebilir.

Bir discriminated union üzerindeki switch'i nasıl kapsamlı (exhaustive) yaparım?

Ya fonksiyona açık bir dönüş tipi verin ve default koymayın (eksik bir case bu durumda TS2366 hatası olur), ya da function assertNever(x: never): never { throw ... } ile birlikte default: return assertNever(value) ekleyin. İkinci biçim, ele alınmayan üyeyi hatada adıyla söyler ve beklenmedik veri gelirse çalışma zamanında da hata fırlatır.

Discriminated union'ım neden daralmıyor?

Etiket her üyede bir literal tip olmalıdır. Bir nesne notasyon olmadan oluşturulduysa kind değeri string tipine genişletilir ve artık hiçbir üyeyle eşleşmez (TS2345/TS2322). Bunu bir notasyonla, as const ile ya da nesneyi union tipinin beklendiği yerde oluşturarak düzeltin. kind: string olarak tiplenmiş bir üye de union'ın kind üzerinden daralmasını durdurur.

Ayırıcı bir boolean veya sayı olabilir mi?

Evet. Her literal tip çalışır: string'ler (kind: "circle"), sayılar (version: 2), boolean'lar (ok: true / ok: false), hatta null veya undefined. String etiketler en yaygın olanlardır, çünkü loglandıklarında ya da JSON olarak gönderildiklerinde okunabilirler.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA