İçeriğe geç
Muhammet Şafak
Diller 4 dk okuma

TypeScript'te tip düzeyinde programlama: utility types

TypeScript'in yerleşik utility type'ları ve tip dönüştürme mekanizmaları: tip sistemini bir araç olarak ileri düzeyde kullanmak.


TypeScript’i ilk öğrendiğinizde, tipler basit etiketler gibi görünür: bu değişken string, bu fonksiyon number döndürür. Ama TypeScript’in tip sistemi çok daha fazlasını yapabiliyor. Tipler üzerinde hesaplamalar yapabilir, mevcut tiplerden yeni tipler türetebilir, conditional type dönüşümleri yazabilirsiniz.

Utility type’lar, TypeScript’in standart kütüphanesinde gelen ve bu dönüştürme işlemlerini kapsayan hazır yapılar. Bunları bilmek, tip sistemiyle gereksiz tekrara düşmeden çalışmanızı sağlıyor.

Partial, Required, Readonly

Üç temel dönüştürme: Partial<T>, Required<T>, Readonly<T>.

interface User {
    id: number
    name: string
    email: string
    bio?: string
}

// Tüm alanlar opsiyonel hale gelir
type UserUpdate = Partial<User>
// { id?: number; name?: string; email?: string; bio?: string }

// Opsiyonel alanlar dahil tümü zorunlu hale gelir
type UserComplete = Required<User>
// { id: number; name: string; email: string; bio: string }

// Hiçbir alan değiştirilemez
type ImmutableUser = Readonly<User>

Partial, genellikle güncelleme (PATCH) isteklerinde ya da kısmi form verilerinde kullanılır. API’ye gönderilecek payload tipi oluştururken oluşturmak yerine, mevcut tip üzerinden türetmek daha güvenilir.

Buradaki pratik değer şu: User interface’ine yeni bir alan eklediğinizde, UserUpdate ve UserComplete otomatik olarak güncelleniyor. Elle türetilmiş iki ayrı tip tutuyorsanız, birini güncelleyip diğerini unutma riski var. Türetilmiş tipler bu riski kaldırıyor.

Pick ve Omit

Bir tipten belirli alanları seçmek ya da çıkarmak:

interface Article {
    id: number
    title: string
    body: string
    authorId: number
    publishedAt: Date | null
    createdAt: Date
}

// Yalnızca seçilen alanlar
type ArticlePreview = Pick<Article, 'id' | 'title' | 'publishedAt'>

// Belirtilen alanlar hariç tümü
type ArticleInput = Omit<Article, 'id' | 'createdAt'>

Bu ikisi API katmanında sık işe yarıyor. Veritabanı modelini doğrudan istemciye döndürmek yerine, hangi alanların görünür olduğunu tip düzeyinde de ifade edebiliyorsunuz.

Pick ve Omit arasında seçim yaparken pratik bir kural edindim: tutmak istediğiniz alan sayısı çıkarmak istediklerinizden azsa Pick, fazlaysa Omit kullanmak kodu daha okunur kılıyor. On alanlı bir tipten dokuzunu almak için Pick yerine Omit kullanmak, amacı daha net aktarıyor.

Record

Anahtar-değer eşlemeleri için:

type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500

type StatusMessages = Record<HttpStatus, string>

const messages: StatusMessages = {
    200: 'OK',
    201: 'Created',
    400: 'Bad Request',
    401: 'Unauthorized',
    404: 'Not Found',
    500: 'Internal Server Error',
}

Record<K, V>, anahtarların K tipinde, değerlerin V tipinde olduğu nesne tipi. Dinamik anahtar kümelerini güvenli biçimde modellemek için kullanılır.

Record’un fark yarattığı yer, anahtar kümesini bir birlik (union) tipiyle tanımladığınızda ortaya çıkıyor. Yukarıdaki örnekte messages nesnesinde 404 anahtarını unutsanız TypeScript hemen uyarıyor. Basit { [key: number]: string } tanımıyla bu denetimi alamazsınız.

Koşullu tipler: infer ile

Utility type’ların altındaki mekanizma conditional type’lar. T extends U ? X : Y sözdizimi, tip düzeyinde if-else.

infer anahtar kelimesi ise conditional type’ların içinde başka bir tipi yakalamak için kullanılır:

// Bir fonksiyonun dönüş tipini çıkar
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

async function fetchUser(id: number): Promise<User> {
    // ...
}

type Result = Awaited<ReturnType<typeof fetchUser>>
// Result = User

Awaited<T> de yerleşik bir utility type; bir Promise<T>’nin sarmaladığı tipi açar. Async fonksiyonlarla çalışırken sık işe yarıyor.

Mapped types ile dönüştürme

Mapped type’lar, bir tipin her alanı üzerinde dönüşüm uygulamanızı sağlar. Utility type’ların çoğu bunun üzerine kurulu:

// Partial'in yaklaşık implementasyonu
type MyPartial<T> = {
    [K in keyof T]?: T[K]
}

// Tüm alanları string'e çevir
type Stringify<T> = {
    [K in keyof T]: string
}

type StringifiedUser = Stringify<User>
// { id: string; name: string; email: string; bio: string }

keyof T bir tipin anahtar birliğini verir; T[K] o anahtarın değer tipini.

Mapped type’ların güçlü olduğu bir alan: form durum tipleri. Bir form modelinin her alanını “değer + hata mesajı” çiftiyle eşleştirmek için standart bir interface yazmak yerine mapped type ile türetebilirsiniz. Interface’e alan eklendiğinde form tipi de otomatik güncelleniyor.

Ne zaman bu kadar gerekli

Tip düzeyinde programlama güçlü bir araç; ama her yerde kullanmak gerekmez. Şu durumlarda gerçekten değer katıyor:

Büyük ve birbiriyle ilişkili tip setlerinde. Bir alanı değiştirdiğinizde, türetilen tipler otomatik güncelleniyorsa manuel senkronizasyon hatası riski ortadan kalkıyor.

Kütüphane veya ortak katman yazarken. Kullanıcıya esnek ama güvenli bir API sunmak için tip dönüştürmeleri gerekiyor.

Runtime ile tip katmanını senkron tutmakta zorlanıldığında. Örneğin Zod veya io-ts şemalarından tip türetmek, ikisini ayrı yazmak yerine tek kaynak prensibiyle çalışmanızı sağlıyor.

Uygulama kodunda her zaman bu karmaşıklık gerekmez. Bazen basit interface tanımları yeterli; tip hesaplamaları her yerde değer katmaz. Dengeyi koda bakan başkasının kolayca anlayıp anlayamayacağı üzerinden kuruyorum.

Etiketler: #TypeScript
Paylaş:

İlgili Yazılar

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi