Şunu itiraf etmeliyim — TypeScript’in utility type’larını ilk öğrendiğimde aklımdan şu geçti: “Bunları gerçek projede ne zaman kullanacağım ki?” Cheatsheet’lerde hep karşına çıkıyor: Partial, Pick, Omit, Record (ki bu çoğu kişinin gözünden kaçıyor). Tamam, güzel. Ama pratikte? Yani gerçek bir SaaS projesi yazarken, Stripe entegrasyonu yaparken, Next.js API route’larında veri döndürürken — o anlarda bu şeyleri aklında tutabiliyor musun? İşte tam da burada, teoride kalan bu utility type’ların gerçek production kodunda nasıl hayat kurtardığını anlatacağım. Kendi deneyimlerimden, hatta dürüst olmak gerekirse hatalarımdan yola çıkarak.
2023 sonunda bir freelance SaaS projesi üzerinde çalışıyordum — subscription yönetimi olan, Stripe ile entegre bir dashboard. İlk başta tipleri elle yazmaya çalıştım, tabi. Sonra üçüncü kez aynı alanları kopyala-yapıştır yaptığımı fark edince duraksadım: “Dur bir dakika, bunun daha akıllı bir yolu olmalı.” Ve gerçekten vardı. Hem de tam orada, gözümün önünde (ciddiyim)
Çok konuştum, örnekle göstereyim.
Partial<T> ile Update İşlemleri: Az Yazıp Çok İş Çıkarmak
SaaS uygulamalarında en sık karşılaşılan senaryo ne biliyor musun? PATCH endpoint’leri (bizzat test ettim). Kullanıcı sadece planını değiştirmek istiyor, sadece ismini güncellemek istiyor ya da sadece e-posta tercihlerini. Tüm alanları zorunlu kılmak saçmalık. İşte tam burada Partial devreye giriyor.
type User = {
id: string;
email: string;
name: string;
plan: 'free' | 'pro';
stripeCustomerId: string;
};
// PATCH endpoint — sadece değişen alanları gönder
async function updateUser(
id: string,
updates: Partial<Omit<User, 'id'>>
) {
return db.update(users).set(updates).where(eq(users.id, id));
}
// Kullanım: sadece plan ve Stripe ID değişti
await updateUser(userId, {
plan: 'pro',
stripeCustomerId: 'cus_abc123'
});
Bakın burada ince bir numara var: Partial<Omit<User, 'id'>> kombinasyonu. Neden? Çünkü birisi gelip update payload’ında id göndermesin istiyorsun. Bu, 2024’te çalıştığım bir projede tam olarak başıma geldi — bir junior developer update fonksiyonuna id alanını da geçirmişti, veritabanında yanlış kayıt güncelleniyordu, kim fark etti dersin, saatlerce sürdü o debug. Partial + Omit birleşimi bu hatayı derleme zamanında yakalıyor. Tabi runtime’da da doğrulama yapman lazım ama tip sistemi ilk savunma hattın oluyor, bunu küçümseme.
Küçük bir startup’ta çalışıyorsan belki “abartıyorsun, 3 alan var zaten” diyebilirsin. Haklısın. O ölçekte belki elle de halledersin, itiraz etmiyorum. Ama 15-20 alanlı bir kullanıcı modelin olduğunda — ki enterprise SaaS’larda bu çok normal, hatta az bile sayılır — Partial olmadan yaşayamazsın. Gerçekten.
Durun, bir saniye.
Pick ve Omit: Frontend’e Ne Kadar Veri Sızdırıyorsun?
Pick — Küçük ve Güvenli Alt Kümeler
Geçen yıl bir güvenlik denetiminde şunu öğrendim: API response’larında gereksiz alan döndürmek, aktif bir güvenlik açığı olmasa bile “information disclosure” kategorisinde değerlendiriliyor (şaşırtıcı ama gerçek). Ciddi mesele bu. Yani veritabanı modelini olduğu gibi frontend’e dönmek… kötü fikir.
type DbUser = {
id: string;
email: string;
passwordHash: string;
stripeCustomerId: string;
plan: 'free' | 'pro';
createdAt: Date;
};
// Tarayıcıya göndermesi güvenli olan kısım
type PublicUser = Pick<DbUser, 'id' | 'email' | 'plan'>;
Pick kullanınca şunu garanti ediyorsun: “Bu tipte sadece bu 3 alan var, başka bir şey sızmıyor.” Runtime’da da tabi bir filtreleme yapman gerekiyor, ama en azından tip sistemi seni uyarıyor. Bir arkadaşım geçen sene Stripe müşteri ID’sini yanlışlıkla frontend’e döndürmüş — zararlı bir şey olmadı ama code review’da yakalandığında bayağı utandı, hatırlarsanız diye de sordu bana sonra. Pick kullanmış olsa derleme zamanında hata alırdı. Mesele bu kadar basit.
Omit — Büyük Tiplerde Daha Pratik
Şimdi gelelim Omit’e. Pick’in tersine çalışıyor: “Şu alanlar hariç hepsini al.” Ne zaman hangisini kullanmalısın?
| Durum | Kullan | Neden |
|---|---|---|
| Güvenli alan sayısı az (2-4) | Pick | Neyin dışarı gittiğini açıkça belirtiyorsun |
| Tehlikeli alan sayısı az (1-2) | Omit | Çoğu alanı zaten göndermek istiyorsun |
| Model sık değişiyor | Pick (daha güvenli) | Yeni eklenen hassas alan otomatik dışarı sızmaz |
| Dahili servisler arası iletişim | Omit | Güvenlik riski daha düşük, pratiklik öne çıkıyor |
Pratik kural: Eğer modeline yeni bir hassas alan eklendiğinde otomatik olarak frontend’e sızmasını istemiyorsan, Pick tercih et. Omit kullanırken yeni eklenen her alanın “varsayılan olarak dışarı açık” olduğunu unutma.
Bu ayrım küçük görünüyor. Ama inan bana, 20+ alanlı bir veritabanı modelinde hayat kurtarıcı — lafı gevelemeden söyleyeyim. Bir keresinde Omit kullanıyordum, sonra modele internalNotes diye bir alan eklendi — müşteri destek notları, gayet hassas bilgiler. Omit listesine eklemeyi unuttum ve bu notlar birkaç saat boyunca API’dan döndü. Neyse ki staging’de yakaladık ama… dersi aldım. Acıyla, ama aldım.
Record<K, V>: Tipli Sözlükler ve Feature Flag Yönetimi
Record tipi belki en “sıkıcı” görüneni. Ama SaaS’ta en çok işe yarayanlardan biri, şaşırdım açıkçası bunu fark edince. Mesela feature flag’ler, plan bazlı ayarlar ve environment ayarları için biçilmiş kaftan. Bu konuyla ilgili Raspberry Pi OLED Ekranları İçin Python Çatısı: Temiz Yol yazımıza da göz atmanızı tavsiye ederim.
// Plan bazlı özellik listesi
const planFeatures: Record<'free' | 'pro' | 'enterprise', string[]> = {
free: ['5 API çağrısı/gün', '1 workspace'],
pro: ['sınırsız API', '10 workspace', 'öncelikli destek'],
enterprise: ['sınırsız her şey', 'SLA', 'SSO'],
};
// Ortam bazlı model ayaru
const modelConfig: Record<'development' | 'staging' | 'production', string> = {
development: 'claude-haiku-4-5-20251001',
staging: 'claude-sonnet-4-6',
production: 'claude-opus-4-6',
};
Record’un asıl gücü şurada: yarın gelip plan tipine 'team' diye yeni bir variant ekle bakalım — TypeScript derleyicisi hemen planFeatures objesinde team anahtarının eksik olduğunu söyleyecek. Bu, runtime’da “undefined” hatası almak yerine derleme zamanında yakalamak demek. Baya güzel, değil mi? Runtime sürprizleri pek sevilmez zaten.
Ha, bir de şunu söyleyeyim — Record’u LLM model konfigürasyonlarında çok kullanıyorum son zamanlarda. AI’yi Projene 2 Saatte Eklemenin Gerçek Hali yazımızda da bahsettiğimiz gibi, farklı ortamlarda farklı modeller kullanmak oldukça yaygın bir pattern. Record tipi bu mapping’i hem type-safe hem de okunabilir hale getiriyor. İkisi birden. Nadir rastlanır.
İşte tam da bu noktada devreye giriyor.
ReturnType<T>: Fonksiyon Dönüş Tiplerini Elle Yazmayı Bırak
Bu utility type’ı keşfettiğim an — abartmıyorum — “neden bunu daha önce kullanmıyordum?” diye kendi kendime söylendim. Mesela Drizzle ORM ya da Prisma gibi araçlarla çalışırken, fonksiyonların döndürdüğü tipleri elle tanımlamak bazen imkansıza yakın bir hal alıyor; ilişkiler, join’ler, select’ler derken tip tanımı 50 satırı geçiyor, okunmaz hale geliyor, kim yazdıysa lanetlenesi bir şey oluyor ortaya çıkan.
async function getSubscriptionWithUser(subscriptionId: string) {
return db.query.subscriptions.findFirst({
where: eq(subscriptions.id, subscriptionId),
with: { user: true, plan: true },
});
}
// Bu fonksiyonun dönüş tipini elle yazmak yerine:
type SubscriptionWithUser = NonNullable
Awaited<ReturnType<typeofgetSubscriptionWithUser>>
>;
Burada üç utility type iç içe kullanılıyor: ReturnType fonksiyonun dönüş tipini çıkarıyor, Awaited async Promise’i çözüyor, NonNullable ise null/undefined ihtimalini kaldırıyor. Karmaşık mı görünüyor? Evet, biraz öyle. Ama alternatifi düşün — ORM’nin döndürdüğü o karmaşık ilişkisel tipi elle yazmak. Hmm… hayır, teşekkürler. Bu konuyla ilgili iPad Air 11 (M4): Fiyatına Göre İyi mi, Fazla mı İddialı? yazımıza da göz atmanızı tavsiye ederim.
Discriminated Union + Extract: Event ve Webhook Yönetimi
Bir de şunu söyleyeyim — SaaS’ta Stripe webhook’larıyla uğraşan herkes bilir bu durumu: gelen event’in tipine göre farklı handler’lar çalıştırman gerekiyor. Discriminated union pattern’i tam burada hayat kurtarıyor. Daha fazla bilgi için Python’la Günlük İşleri Otomatikleştirin: Evde, İşte, Hayatta yazımıza bakabilirsiniz.
type WebhookEvent =
| { type: 'checkout.session.completed'; data: { sessionId: string; customerId: string } }
| { type: 'invoice.payment_failed'; data: { invoiceId: string; attemptCount: number } }
| { type: 'customer.subscription.deleted'; data: { subscriptionId: string } };
// Sadece belirli bir event tipini çekmek istiyorsan:
type PaymentFailedEvent = Extract<WebhookEvent, { type: 'invoice.payment_failed' }>;
function handlePaymentFailed(event: PaymentFailedEvent) {
console.log(`Ödeme başarısız, deneme: ${event.data.attemptCount}`);
// Kullanıcıya e-posta gönder, retry logic çalıştır vs.
}
Extract burada zarif bir iş yapıyor — union tipinden sadece istediğin varyantı çekip çıkarıyor. Bunu elle yapmaya çalışsan her event tipi için ayrı bir interface tanımlaman gerekir, sonra bunları bir union’da birleştirmen gerekir, sonra bakımı gerekir, sonra birisi gelip değiştirir, sonra… Neyse. Extract ile tek satırda halloluyor.
2024 başında bir Stripe entegrasyonu yazarken tam 12 farklı webhook event tipi yönetmem gerekti. On iki. Extract olmadan her birinin tipini ayrı ayrı tanımlamam gerekirdi. Extract ile sadece ana union’ı yazdım, gerisini TypeScript halletti. Express.js Güvenlik Testinde Dört Araç Birbirini Nasıl Doğruladı? yazımızda da benzer pattern’leri güvenlik bağlamında ele almıştık.
Pratikte Dikkat Edilmesi Gerekenler
Aşırı Soyutlama Tuzağı
Açık konuşayım — bu utility type’ları öğrenince her yerde kullanmak istiyorsun. Normal. Ama bazen basit bir interface tanımı yeterli. Partial<Pick<Omit<Extract<...>>>> gibi dört katmanlı tip zincirleri yazmaya başladıysan, dur bir saniye — okunabilirlik kayboluyor olabilir, bak bir daha.
Aslında, Kural şu: eğer bir meslektaşın tip tanımını okuyup 10 saniyede anlayamıyorsa, muhtemelen parçalaman gerekiyor. Ara tipler tanımla, anlamlı isimler ver. type SafeUserForDashboard = Pick<DbUser, 'id' | 'email' | 'plan'> gibi açıklayıcı isimler kullan. Hani ne farkı var diyorsunuz, değil mi? Kodun okunması da bir beceri, sakın küçümseme.
Runtime Güvenliğini Unutma
TypeScript’in tip sistemi derleme zamanında çalışıyor — runtime’da her şey düz JavaScript. Yani Pick ile “bu 3 alanı dön” desen bile, eğer runtime’da objeyi filtrelemezsen tüm alanlar yine dönecek (kendi tecrübem). Tip sistemi bir güvenlik ağı ama tek başına yeterli değil, bu çok önemli. Zod, io-ts gibi runtime validation kütüphaneleriyle birlikte kullanman şart.
Az önce “tip sistemi yeterli” gibi bir izlenim verdim, ama aslında tam da bu noktada dikkatli olmak lazım. Tip sistemi “bu olması gereken” diyor, runtime validation ise “bu gerçekten olan” diyor (yanlış duymadınız). İkisi birlikte çalışmalı. Biri olmadan diğeri eksik kalıyor.
Sıkça Sorulan Sorular
Partial ve Required arasındaki fark nedir?
Partial<T> tüm alanları opsiyonel yapar, Required<T> ise tam tersi — tüm alanları zorunlu kılar. Eğer bir tipin bazı alanları zaten opsiyonelse ve bunları zorunlu hale getirmek istiyorsan Required kullanırsın. Pratikte Partial çok daha sık kullanılıyor çünkü update/patch senaryoları daha yaygın.
Pick mı Omit mi kullanmalıyım? Hangisi daha güvenli?
Güvenlik açısından Pick genellikle daha güvenlidir çünkü “whitelist” mantığıyla çalışır — sadece belirttiğin alanlar geçer. Omit ise “blacklist” mantığıyla çalışır ve modele yeni eklenen alanlar otomatik dahil olur. Frontend’e veri dönerken Pick, dahili servisler arası iletişimde Omit tercih edilebilir.
ReturnType async fonksiyonlarda nasıl çalışır?
Async fonksiyonlar Promise döndürdüğü için ReturnType sana Promise<T> tipini verir (evet, doğru duydunuz). Promise’in içindeki asıl tipi almak için Awaited<ReturnType<typeoffn>> şeklinde Awaited ile sarmalaman gerekiyor. TypeScript 4.5 ile gelen Awaited tipi bu işi bayağı kolaylaştırdı.
Bu utility type’lar JavaScript’e derleniyor mu?
Küçük bir detay: Hayır. TypeScript utility type’ları büyük ölçüde derleme zamanında çalışır — kendi adıma konuşayım — ve JavaScript çıktısında hiçbir iz bırakmaz. Yani performans maliyeti sıfır — sadece geliştirici deneyimini iyileştiriyorlar. Runtime’da ek kontrol istiyorsan Zod gibi bir kütüphane kullanman gerekiyor.
Record tipi ile Map arasındaki fark nedir?
Record bir tip tanımıdır ve düz JavaScript objelerine derlenir (yanlış duymadınız). Map ise runtime’da çalışan bir veri yapısıdır — iterasyon sırası garantisi verir, herhangi bir tipi anahtar olarak kullanabilir. Eğer anahtarların string literal union ise Record, dinamik anahtarlarla çalışıyorsan Map daha uygun olur.
Kaynaklar ve İleri Okuma
TypeScript Handbook — Utility Types (Resmi Dokümantasyon)
İnanın, Type Challenges — TypeScript Tip Egzersizleri (GitHub)
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.



