Her projenin o klasik elemanı vardır: fetch çağrısı, arkasından as ileecast edilen bir tip. Derleyici memnundur, tip dosyada yazılıdır — ama sunucu bir gün data yerine dada gönderdiğinde uygulama çalışma anında patlar. Sorunun adı şu: tip, gerçeklikle runtime'da değil compile-time'da karşılaşıyor. Çözüm de tek cümlelik: şemayı bir kez yaz, tipleri ondan üret, sınırda doğrula.

Şema tek gerçek kaynağı olsun

Zod gibi bir kütüphanede şema hem doğrulayıcı hem tip üreticidir. Şemayı bir kez tanımlarsınız; TypeScript tipi ondan türetilir. Böylece "dokümandaki tip" ile "kabul edilen tip" arasında kopukluk doğmaz:

// lib/schemas.ts
import { z } from 'zod';

// API'nin gerçekten döndüğü şekil — TEK GERÇEK KAYNAK
export const Makale = z.object({
  id: z.string().uuid(),
  baslik: z.string().min(1),
  ozet: z.string().max(300),
  yayinda: z.boolean(),
  etiketler: z.array(z.string()).default([]),
  yayinTarihi: z.string().datetime(), // ISO 8601
});

// Tipi elle yazmak YOK; şemadan üretiliyor:
export type Makale = z.infer<typeof Makale>;

z.infer sayesinde şemada yaptığınız her değişiklik, tiplere anında yansır. Bir alanı zorunlu yaptığınızda onu okuyan bütün kod derleme anında uyarılır — production'da değil, editörde.

Sınırda doğrula, sonra tipli veriyle çalış

Doğrulama, verinin dışarıdan geldiği her yerde yapılmalı: API yanıtı, form verisi, localStorage, URLSearchParams. Aşağıdaki apiGet, başarılı her yanıtı Makale şemasından geçirir; şema bozulursa hata fırlatır ve fonksiyon asla yanlış tipli veriyi döndürmez:

// lib/api.ts
import { Makale, type Makale } from './schemas';

export class ApiHata extends Error {
  constructor(public detaylar: unknown) {
    super('API yanıtı şemaya uymadı');
  }
}

export async function makaleGetir(id: string): Promise<Makale> {
  const res = await fetch(`/api/articles/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json: unknown = await res.json(); // bilinçli olarak unknown

  // Sınırda doğrulama: tip güvenliği BURADA kuruluyor.
  const sonuc = Makale.safeParse(json);
  if (!sonuc.success) throw new ApiHata(sonuc.error);

  return sonuc.data; // artık tip: Makale
}

Birkaç inceliğe dikkat: res.json() sonucunu unknown'a alıyoruz — any değil. unknown'un kullanılmadan önce daraltılması gerekir; bu yüzden şema doğrulaması olmadan derleyici zaten geçit vermez. İkincisi, safeParse başarısızsa hatayı yutmuyoruz: ApiHata ile loglanabilir bir iz bırakıyoruz. Üçüncüsü, dönüş tipi Promise<Makale> — çağıran taraf için artık "umluyorum" değil "bunu alıyorum" sözü var.

Hata akışını da tiplendirebilirsiniz. Birleşik tip (union) ile "başarı" ve "başarısızlık" hallerini fonksiyon imzasına yazarsınız; çağıran taraf if ile ayrımadan derleyici geçit vermez:

// Sonucu birleşik tip ile modellemek: hata yönetimi tip sisteminin parçası
type Sonuc<T> = { ok: true; veri: T } | { ok: false; hata: string };

export async function guvenliMakale(id: string): Promise<Sonuc<Makale>> {
  try {
    return { ok: true, veri: await makaleGetir(id) };
  } catch (e) {
    return { ok: false, hata: e instanceof Error ? e.message : 'bilinmeyen' };
  }
}

// Çağıran taraf:
const s = await guvenliMakale(id);
if (s.ok) console.log(s.veri.baslik); // s.veri burada kesin Makale
else console.error(s.hata);           // derleyici ok'u zorunlu tutuyor

Bedeli ve getirisi

Ek maliyet gerçek: şema yazmak, hata yönetimini düşünmek, unknown'la uğraşmak. Buna karşılık getirisi somut: field adı değişince derleyici patlıyor, "undefined hatası production'da" teması azalıyor, dokümantasyon ihtiyacı şemaya devrediliyor. Runtime verisiyle compile-time tipleri aynı kaynaktan akıttığınızda tip güvenliği bir lütuf değil, mimarinin doğal sonucu haline geliyor.