selupucin
Web Geliştirme

TypeScript ile Uçtan Uca Tip Güvenliği

Form, doğrulama, API ve veritabanı zincirini tek şemadan besleyerek tip hatalarını canlıdan derleme zamanına çeken pratik bir kurulum.

Deniz Selupuçin7 dk okuma

Tip güvenliği bir ayar değil, bir zincir

TypeScript'i projeye eklemek tek başına hiçbir şey garanti etmez. Gerçek kazanç, verinin geçtiği zincirin tamamı aynı tanımdan besleniyorsa ortaya çıkar: kullanıcının doldurduğu form, ağdan geçen istek, sunucudaki doğrulama, veritabanına yazılan belge ve arayüze dönen yanıt.

Bu zincirde tek bir halka bile elle yazılmış ikinci bir tanıma dayanıyorsa, orası er ya da geç sessizce kayar. Alan adı değişir, zorunlu bir alan opsiyonele döner, sayı beklenen yere metin gelir — ve hata canlıda, kullanıcı formu gönderdiği anda ortaya çıkar. Üstelik çoğu zaman en pahalı hâliyle: hiç patlamadan, sessizce bozuk veri kaydederek.

Bu yazıda gerçek projelerde kurduğum zinciri anlatıyorum: tek şema kaynağından başlayıp, hataların hangi noktada derleme zamanına çekildiğine kadar.

Sınırlar: tipin bittiği yer

TypeScript yalnızca derleme zamanında vardır; çalışma zamanında hiçbir kontrolü yoktur. Bu yüzden uygulamanın "sınır" noktalarında tip bilgisi tamamen kaybolur:

  • Form gönderimi: FormData içindeki her değer metin ya da dosyadır.
  • HTTP yanıtları: response.json() size any verir.
  • Veritabanından dönen belgeler: Şema dışı ya da eski alanlar hâlâ gelebilir.
  • Ortam değişkenleri, URL parametreleri, üçüncü taraf webhook'ları.

Bu sınırlarda yapılan en yaygın hata, veriyi as ile "tipli" ilan etmektir:

// Yanlış: bu bir kontrol değil, bir temenni
const body = (await request.json()) as ContactInput;

as, derleyiciye "sus" demektir. Veri gerçekten o şekilde değilse hiçbir şey olmaz — ta ki üç katman sonra var olmayan bir alana erişilene kadar. Doğru yaklaşım, sınırda çalışma zamanı doğrulaması yapıp tipi oradan türetmektir.

Tek kaynak: önce şema, sonra tip

Zinciri Zod şemasıyla başlatıyorum. Şema hem çalışma zamanında doğrular hem de TypeScript tipini üretir; böylece iki ayrı tanımı senkron tutma derdi ortadan kalkar.

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

export const contactSchema = z.object({
  name: z.string().min(2).max(80),
  email: z.string().email(),
  subject: z.enum(["proje", "destek", "diger"]),
  message: z.string().min(20).max(2000),
  kvkkConsent: z.literal(true),
});

// Tip elle yazılmaz — şemadan türetilir.
export type ContactInput = z.infer<typeof contactSchema>;

Kritik ayrıntı şu: ContactInput elle yazılmış bir arayüz değil. Şemaya bir alan eklediğinizde tip anında değişir ve o alanı işlemeyen her yer derleme hatası verir. Tanımı ikiye bölseydiniz, ikinci tanım sessizce eskiyecekti.

Bu tek kaynak kuralını pratikte şöyle uyguluyorum:

  • Şemalar tek bir klasörde yaşar; hem sunucu hem istemci oradan içe aktarır.
  • Hiçbir yerde şemanın elle yazılmış "kopyası" bir arayüz olarak durmaz.
  • Türev tanımlar da şemadan gelir: contactSchema.pick(...), .omit(...), .partial().

Sunucu-istemci sınırı: Server Action girişleri

Server Action'lar tip güvenliği açısından yanıltıcıdır: fonksiyon imzası tipli görünür ama çağrı ağdan geçer. Yani istemciden gelen veri, tıpkı bir API rotasındaki gibi güvenilmezdir. Sınırda mutlaka doğrulayın:

"use server";
import { contactSchema } from "@/lib/schemas/contact";

export async function submitContact(
  _prev: FormState,
  formData: FormData,
): Promise<FormState> {
  const parsed = contactSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    subject: formData.get("subject"),
    message: formData.get("message"),
    kvkkConsent: formData.get("kvkkConsent") === "on",
  });

  if (!parsed.success) {
    return { ok: false, fieldErrors: parsed.error.flatten().fieldErrors };
  }

  // Buradan sonrası tam tipli: parsed.data → ContactInput
  await createContactMessage(parsed.data);
  return { ok: true };
}

safeParse sonrası TypeScript'in tip daraltması (narrowing) devreye girer ve parsed.data artık tam tiplidir — as yok, any yok. Hata dalında dönen alan hataları da aynı şemadan üretildiği için form alan adlarıyla birebir eşleşir; arayüzde hatayı yanlış input'a bağlama ihtimali kalmaz.

Formda aynı şemayı yeniden kullanmak

İstemci tarafı doğrulama bir kullanıcı deneyimi kolaylığıdır, güvenlik sınırı değildir. Ama aynı şemayı kullanmak zorundadır. İkinci bir kural seti yazmak en sinsi tutarsızlık kaynağıdır: sunucu 20 karakter isterken istemci 10 karaktere izin verir, kullanıcı formu doldurur, sunucu reddeder ve kimse nedenini anlamaz.

"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { contactSchema, type ContactInput } from "@/lib/schemas/contact";

const form = useForm<ContactInput>({ resolver: zodResolver(contactSchema) });

Kural basit: doğrulama kuralı tek yerde tanımlanır, iki yerde çalıştırılır.

API yanıtlarını tiplemek

Giden veriyi doğrulamak yetmez; gelen veri de bir sınırdır. Özellikle üçüncü taraf servislerde yanıt şeması size haber verilmeden değişir.

const emailResponseSchema = z.object({
  id: z.string(),
  status: z.enum(["queued", "sent", "failed"]),
});

export async function sendEmail(input: ContactInput) {
  const res = await fetch(EMAIL_ENDPOINT, { method: "POST", body: JSON.stringify(input) });
  // `as` değil, parse: yanlış yanıt kaynağında patlar, üç katman sonra değil.
  return emailResponseSchema.parse(await res.json());
}

Kendi sunucu eylemlerinizin ve rotalarınızın çıktısını da tiplemek gerekir. Bunun için ayrımlı birleşim (discriminated union) kullanıyorum:

export type ActionResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string; fieldErrors?: Record<string, string[]> };

Bu tipi tüketen hiçbir bileşen, ok kontrolünü yapmadan data alanına erişemez. Hata yolunu ele almayı unutmak artık bir çalışma zamanı sürprizi değil, doğrudan bir derleme hatasıdır. Optimistic UI ve retry gibi akışlarda bu tek karar, ele alınmamış hata durumlarının neredeyse tamamını ortadan kaldırıyor.

Veritabanı modeliyle şemayı hizalamak

Zincirin son halkası veritabanı. Mongoose kullanıyorsanız iki tanım kaçınılmaz görünür: Zod şeması ve Mongoose şeması. Birini diğerinden otomatik üretmek çoğu projede aşırı mühendisliktir; onun yerine hizayı teste bağlıyorum.

Önce model tipini şemadan türetin, elle yazmayın:

import mongoose, { type InferSchemaType } from "mongoose";

const contactMessageSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true },
  subject: { type: String, required: true },
  message: { type: String, required: true },
});

export type ContactMessageDoc = InferSchemaType<typeof contactMessageSchema>;

Sonra iki tarafın uyumunu bir tip testiyle sabitleyin:

// tests/schema-alignment.test-d.ts
import type { ContactInput } from "@/lib/schemas/contact";
import type { ContactMessageDoc } from "@/models/ContactMessage";

// Zod şemasındaki her alan, modelde de aynı tiple duruyor mu?
const _alignment: Pick<ContactMessageDoc, keyof ContactInput> =
  {} as ContactInput;

Bu birkaç satır, alan adı bir tarafta değişip diğerinde değişmediğinde pnpm typecheck adımını kırar. Yani sapmayı (drift) "dikkat ederiz" ile değil, CI ile engellemiş olursunuz. Aynı disiplini bu projede tema token'ları ve önbellek etiketleri için de uyguluyorum: tanım tek yerde durur, sapma derlemede yakalanır.

any'den kaçınmak: unknown, daraltma ve satisfies

any bulaşıcıdır. Bir değere any dediğiniz anda ondan türeyen her şey denetim dışına çıkar; tipli bir kod tabanının içinde tipsiz bir ada oluşur. Üç pratik alternatif:

  • unknown: Ne geldiğini bilmiyorsanız doğru tip budur. Kullanmadan önce sizi daraltmaya zorlar.
  • Tip koruyucular ve daraltma: typeof kontrolü, in operatörü ya da şema doğrulaması ile tipi kazanın.
  • satisfies: Bir nesnenin sözleşmeye uyduğunu, gerçek tipini genişletmeden doğrular. Sabit tablolarda (rota haritası, önbellek etiketleri, tema token'ları) çok işe yarar.
export const revalidateTags = {
  posts: "posts",
  services: "services",
} satisfies Record<string, string>;

// tags.posts hâlâ "posts" literal tipinde: sözleşme kontrolü var, daralma yok.

ESLint tarafında no-explicit-any kuralını uyarı değil hata seviyesine çekin. Gerçekten kaçınılmaz olan tek tük yerde açık bir gerekçe yorumuyla bastırın; sessiz istisna bırakmayın.

strict yeterli değil: açılması gereken ayarlar

strict: true başlangıç noktasıdır, bitiş değil. Kullandığım ek ayarlar:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "verbatimModuleSyntax": true
  }
}

En çok işe yarayanı noUncheckedIndexedAccess. Dizi ve sözlük erişimlerini otomatik olarak "olmayabilir" kabul eder; üretimde en sık gördüğüm "tanımsız değer okuma" hatalarının büyük kısmını daha kod yazılırken engeller. İlk açtığınızda onlarca hata görürsünüz — hepsi zaten var olan, henüz patlamamış hatalardır.

Asıl fayda: hatayı zamanda geriye çekmek

Bir hatanın maliyeti, keşfedildiği ana bağlıdır. Aynı hata:

  • Editörde yakalanırsa saniyeler,
  • CI'da yakalanırsa dakikalar,
  • Canlıda yakalanırsa saatler ve güven kaybı,
  • Hiç yakalanmazsa sessizce bozulmuş veri.

Uçtan uca tip güvenliğinin bütün işi bu: hatayı zaman ekseninde olabildiğince sola çekmek. Somut karşılıkları şunlar:

  • Korkusuz refactor. Bir alanı yeniden adlandırdığınızda etkilenen her dosya anında listelenir; "acaba bir yeri unuttum mu" endişesi biter.
  • Daha az savunma kodu. Doğrulama sınırda bir kez yapıldığı için iç katmanlardaki "acaba var mı" kontrolleri erir, kod okunur kalır.
  • Belge olarak tip. Şema, bir alanın ne olduğunu ve hangi kuralla doğrulandığını tek yerde anlatır; ayrı bir doküman eskimez çünkü yoktur.
  • Daha güvenli veri. Doğrulanmamış veri veritabanına hiç ulaşmaz. Kişisel veri işleyen formlarda bu aynı zamanda bir uyum gerekliliğidir; o tarafı KVKK uyumlu web sitesi kontrol listesi yazısında ayrıca ele aldım.

Bakım tarafındaki farkı Next.js mi WordPress mi karşılaştırmasında da yazmıştım: tipli bir kod tabanında bir şeyin bozulduğunu canlıda değil, derleme anında görürsünüz. Sunucu bileşenleri (RSC) ile veri sunucuda çözülüp arayüze tipli biçimde aktığında bu avantaj daha da belirginleşir.

Mevcut projede kademeli geçiş

Sıfırdan başlamıyorsanız bu zinciri bir hafta sonunda kuramazsınız; kurmaya çalışmak da gereksiz. İzlediğim sıra şu:

  1. Önce sınırları kapatın. Dışarıdan veri alan her noktayı listeleyin ve en riskli olandan başlayın: kullanıcı formları, ödeme ve e-posta entegrasyonları, webhook'lar. Bu adım tek başına gerçek hataların büyük kısmını keser.
  2. Sonra tipleri şemaya bağlayın. Elle yazılmış arayüzleri tek tek silip yerlerine türetilmiş tipleri koyun. Her silme işlemi size bir sapma noktası gösterir; onlar zaten var olan, henüz fark edilmemiş hatalardır.
  3. Ardından derleyici ayarlarını sıkın. Hepsini birden değil, teker teker açın; her ayarın çıkardığı hataları temizleyip bir sonrakine geçin.
  4. En son kilidi vurun. Tip kontrolünü CI'da zorunlu adım yapın ve şema-model hizasını teste bağlayın. Bu adım olmadan yukarıdaki üç madde zamanla geri kayar.

Bu sırayı bozmanın maliyeti yüksektir. Ayarları en başta sıkarsanız yüzlerce hatayla boğulur, ekip de kuralı devre dışı bırakmanın yolunu bulur. Küçük ve kapanan adımlar, tipli bir kod tabanına giden tek sürdürülebilir yol.

Özet kontrol listesi

  • Her sınırda (form, API rotası, webhook, ortam değişkeni) çalışma zamanı doğrulaması var.
  • Tipler şemadan z.infer ile türetiliyor; elle yazılmış ikinci tanım yok.
  • İstemci ve sunucu aynı şemayı kullanıyor, kural kopyalanmıyor.
  • Dış yanıtlar as ile değil, parse ile tipleniyor.
  • Sonuç tipleri ayrımlı birleşim; hata yolu unutulamıyor.
  • Veritabanı modeli ile şema hizası bir tip testiyle kilitli.
  • any yasak; unknown, daraltma ve satisfies kullanılıyor.
  • strict ötesi ayarlar açık; tip kontrolü CI'da zorunlu adım.

Tip güvenliği bir lüks değil; tek kişilik bir ekipte bile işe yarayan bir sigortadır. Bu zinciri sıfırdan kurmak ya da mevcut bir kod tabanını kademeli olarak buraya taşımak istiyorsanız, web uygulaması geliştirme yaklaşımıma göz atabilir ya da projenizi konuşmak için yazabilirsiniz.

Devamı

İlgili yazılar

Web Geliştirme
9 dk okuma

KVKK Uyumlu Web Sitesi Kontrol Listesi (2026)

Aydınlatma metninden çerez banner'ına, veri saklamadan güvenlik tedbirlerine kadar bir siteyi yayına almadan önce geçtiğim KVKK kontrol listesi.

Web Geliştirme
7 dk okuma

Next.js App Router ile Güçlü SEO Kurulumu

metadataBase'den kanonik kurguya, parçalı sitemap'ten JSON-LD ve OG görseline: App Router'da sağlam bir SEO temelini adım adım nasıl kurduğumu anlatıyorum.

Sırada ne var?

Bir fikriniz mi var? Hayata geçirelim.

merhaba@selupucin.com

Genelde 24 saat içinde dönüş yapıyorum.