Mert Özen Her Satırda Daha İleri
Frontend TypeScript Orta — #07

TypeScript Utility Types 1: Partial, Required, Readonly, Pick, Omit ve Record

Mert Özen 3 Kas 2026 8 dk 0 görüntülenme
TypeScript Utility Types 1: Partial, Required, Readonly, Pick, Omit ve Record

Partial, Required, Readonly, Pick, Omit ve Record'u gerçekçi örneklerle kullanıyor, bir form modelinde birleştiriyor ve Omit'teki yazım hatası gibi sessiz tuzakları görüyoruz.

Sürüm notu: Bu yazıdaki kodlar TypeScript 7.0 ve Node 26 içindir.

Önce Tahmin Et

Bir kullanıcının bilgilerini güncelleyen bir fonksiyonumuz var. Güncelleme sırasında bütün alanları göndermek istemiyoruz, sadece değişenleri:

type User = { id: number; name: string; email: string };

function updateUser(id: number, changes: Partial<User>) {
  console.log(id, changes);
}

updateUser(1, { name: "Grace" });
updateUser(1, {});
updateUser(1, { age: 30 });

Üç çağrıdan hangileri derlenir? Hata veren varsa, TypeScript neye itiraz eder?

Utility Type Nedir?

Utility type'lar, TypeScript'in standart kütüphanesinde hazır gelen generic type'lardır. Bir fonksiyon nasıl bir değer alıp yeni bir değer döndürüyorsa, bunlar da bir type alıp yeni bir type döndürür. Hiçbirini import etmen gerekmez; her TypeScript dosyasında zaten vardırlar.

İçlerinde sihir yok. Örneğin Partial, TypeScript'in kendi tanım dosyasında aşağı yukarı şöyle yazılmıştır:

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

Bu bir mapped type: T'nin her key'i için bir property üretir, her birine ? ekler ve tipini bir önceki yazıda gördüğümüz T[P] ile orijinalinden alır. Mapped type'ları İleri seride ayrıntılı göreceğiz ve bu yazıdakilerin hepsini kendimiz yazacağız. Şimdilik onları hazır araçlar gibi kullanacağız.

Akılda tutman gereken model şu: utility type'lar orijinal type'ı değiştirmez. Bir fotoğrafa filtre uyguladığında orijinal fotoğraf aynı kalır, sen yeni bir kopya alırsın. Partial<User> yazdığında User yine aynı User'dır; yanında yeni bir type daha olur.

Partial ve Required

Partial<T> bütün property'leri optional yapar. Baştaki soruya dönelim: ilk iki çağrı derlenir. { name: "Grace" } geçerli, çünkü diğer alanlar artık zorunlu değil. Boş object {} de geçerli, çünkü hiçbir alan zorunlu değil. Üçüncü çağrı ise derlenmez:

error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'Partial<User>'.

Partial alanları optional yapar ama yeni alan kabul etmez; Temel seride gördüğümüz fazla property kontrolü burada da çalışır. Tipik kullanımı bir "yama" (patch) object'idir:

type User = { id: number; name: string; email: string };

function applyChanges(user: User, changes: Partial<User>): User {
  return { ...user, ...changes };
}

const ada: User = { id: 1, name: "Ada", email: "ada@example.com" };
console.log(applyChanges(ada, { email: "ada@lovelace.dev" }));
// { id: 1, name: 'Ada', email: 'ada@lovelace.dev' }

Required<T> bunun tersidir: bütün ?'leri kaldırır. En sık kullanıldığı yer, optional ayarların varsayılanlarla doldurulduğu yerdir. Dışarıdan gelen ayarlar eksik olabilir, ama varsayılanlar eksiksiz olmalıdır:

type Options = {
  retries?: number;
  timeoutMs?: number;
};

const defaults: Required<Options> = { retries: 3, timeoutMs: 5000 };

function resolveOptions(options: Options): Required<Options> {
  return { ...defaults, ...options };
}

console.log(resolveOptions({ retries: 5 })); // { retries: 5, timeoutMs: 5000 }

// Hata: Property 'timeoutMs' is missing in type '{ retries: number; }' but required in type 'Required<Options>'.
const incomplete: Required<Options> = { retries: 1 };

Biri Options'a yeni bir ayar eklediğinde defaults satırı hemen hata verir ve varsayılan değeri yazmayı unutamazsın.

Readonly

Readonly<T> bütün property'leri readonly yapar. Uygulama boyunca değişmemesi gereken bir yapılandırma için iyi bir seçimdir:

type AppConfig = {
  apiUrl: string;
  features: { beta: boolean };
};

const config: Readonly<AppConfig> = {
  apiUrl: "https://api.example.com",
  features: { beta: false },
};

// Hata: Cannot assign to 'apiUrl' because it is a read-only property.
config.apiUrl = "https://other.example.com";

config.features.beta = true; // derlenir!

const tags: Readonly<string[]> = ["ts", "js"];
// Hata: Property 'push' does not exist on type 'readonly string[]'.
tags.push("go");

Ortadaki satıra dikkat et: Readonly sığdır (shallow). Sadece ilk seviyedeki property'leri kilitler; features'ı başka bir object ile değiştiremezsin, ama içindeki beta'yı değiştirebilirsin. İç içe yapıları tamamen kilitlemek için kendi DeepReadonly'ni yazman gerekir; bunu İleri seride recursive type'larla yapacağız.

Bir de şunu bil: readonly sadece derleme zamanındaki bir kuraldır. Çalışma zamanında object donmaz. Gerçekten dondurmak istiyorsan JavaScript serilerinden tanıdığın Object.freeze'i kullanırsın; TypeScript onun dönüş tipini zaten Readonly<T> olarak tanır.

Pick ve Omit

Bu ikisi bir type'tan property seçer. Pick<T, K> sadece istediğin key'leri alır, Omit<T, K> istediğin key'leri atıp gerisini alır. Hangisini kullanacağın, hangi listenin daha kısa olduğuna bağlı:

type User = {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
};

type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }

type PublicUser = Omit<User, "passwordHash">;
// { id: number; name: string; email: string; }

function toPublic(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

// Hata: Type '"avatar"' does not satisfy the constraint 'keyof User'.
type WithAvatar = Pick<User, "avatar">;

PublicUser güzel bir örnek: dışarıya göndereceğin cevapta şifre özeti olmasın istiyorsun. toPublic fonksiyonunda passwordHash'i atmayı unutsan, dönüş değeri hâlâ PublicUser'a atanabilirdi; çünkü fazla property'si olan bir object, fazla property kontrolüne takılmadığı sürece atanabilir. Yani Omit bir type'ı tarif eder, verinin içinden alanı silmez. Silme işini ...rest ile sen yaparsın.

Pick'te key'ler keyof T ile kısıtlıdır, bu yüzden var olmayan bir key yazdığında TS2344 hatası alırsın. Omit'in böyle bir kısıtı yok; bunun sonucunu tuzaklar bölümünde göreceğiz.

Record

Record<K, V> key'leri K, value'ları V olan bir object type'ı üretir. Temel seride index signature'ı anlatırken ona işaret etmiştik. Asıl gücü, K bir literal union olduğunda ortaya çıkar:

type Role = "admin" | "editor" | "viewer";

const permissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

// Hata: Property 'viewer' is missing in type '{ admin: string[]; editor: string[]; }' but required in type 'Record<Role, string[]>'.
const partialPermissions: Record<Role, string[]> = {
  admin: ["read"],
  editor: ["read"],
};

const visits: Record<string, number> = {};
visits["/home"] = 1;
visits["/about"] = 3;

Role'e yeni bir rol eklendiğinde permissions object'i hemen hata verir: yeni rolün izinlerini yazmadan derleyemezsin. Bir union'ın her üyesi için bir değer tanımlamanın en kısa ve en güvenli yolu bu. Record<string, number> ise key'leri önceden bilinmeyen sözlükler içindir ve { [key: string]: number } ile aynı anlama gelir.

Birlikte Kullanmak: Bir Form Modeli

Utility type'lar asıl değerini birbirleriyle birleştiğinde gösterir. Bir ürün formu düşünelim: kayıtlı ürünün id'si ve createdAt'i var, ama yeni ürün oluştururken bunları kullanıcı göndermez. Formun hata mesajları da alan alan tutulsun:

type Product = {
  id: string;
  title: string;
  price: number;
  stock: number;
  createdAt: Date;
};

type NewProduct = Omit<Product, "id" | "createdAt">;
type ProductPatch = Partial<NewProduct>;
type ProductErrors = Partial<Record<keyof NewProduct, string>>;

function validate(input: NewProduct): ProductErrors {
  const errors: ProductErrors = {};
  if (input.title.trim() === "") errors.title = "Title is required";
  if (input.price <= 0) errors.price = "Price must be positive";
  return errors;
}

const patch: ProductPatch = { stock: 0 };
console.log(validate({ title: "", price: 0, stock: 5 }));
// { title: 'Title is required', price: 'Price must be positive' }

Dört type, tek bir kaynaktan türedi. Product'a bir category alanı eklediğinde NewProduct, ProductPatch ve ProductErrors kendiliğinden güncellenir. ProductErrors satırı ilk bakışta karışık görünebilir, içten dışa oku: keyof NewProduct alan adlarının union'ı, Record her alana bir string mesaj verir, Partial da "her alanda hata olmak zorunda değil" der.

Sık Düşülen Tuzaklar

type User = { id: number; name: string; email: string };

type WithoutEmail = Omit<User, "emial">; // derlenir, email hâlâ içinde

type Options = { retries?: number };
const defaults: Required<Options> = { retries: 3 };

function resolve(options: Options): Required<Options> {
  return { ...defaults, ...options };
}

console.log(resolve({ retries: undefined })); // { retries: undefined }

Birinci tuzak Omit'teki yazım hatası. Omit'in ikinci parametresi keyof T ile değil, herhangi bir key ile kısıtlıdır. "emial" diye yazdığında derleyici itiraz etmez, hiçbir şey atılmaz ve WithoutEmail düpedüz User olur. Key adlarını editörün otomatik tamamlamasıyla yazmak bu hatanın en basit ilacı.

İkinci tuzak daha sinsi. retries?: number yazdığında, varsayılan ayarlarla retries'a açıkça undefined vermek de serbesttir. Spread ile birleştirince bu undefined varsayılan değerin üzerine yazar, ama TypeScript dönüş tipini yine de number kabul eder. Çıktıda undefined görürsün. Bunu kapatan exactOptionalPropertyTypes ayarı strict ailesine dahil değil. tsc --init'in ürettiği tsconfig'te açık gelir, ama kendi yazdığın bir tsconfig'te ayrıca açman gerekir. Açık değilse, varsayılanları birleştirirken undefined değerleri ayıklamayı düşün.

Üçüncüsü bir hatırlatma: Readonly sığdır ve hiçbir utility type çalışma zamanında bir şey yapmaz. Partial, Omit ya da Readonly derlenmiş JavaScript'te iz bırakmaz; veriyi değiştirmek ya da kontrol etmek hâlâ senin kodunun işi.

Küçük Bir Deneme

★☆☆ type Post = { id: number; title: string; body: string; published: boolean }; için bir liste ekranında kullanacağın, sadece id ve title içeren PostSummary type'ını önce Pick, sonra Omit ile yaz. Hangisi daha kısa oldu?

★★☆ Derlenir mi, tahmin et: const p: Readonly<Post> = { id: 1, title: "Hi", body: "", published: false }; p.published = true; Peki const q: Partial<Readonly<Post>> = {}; q.title = "x";?

★★★ type Theme = "light" | "dark" | "contrast"; için her tema adına bir renk kodu eşleyen bir Record yaz. Sonra Theme'e "sepia" ekle ve derleyicinin seni nerede durdurduğunu gör.

Bu yazıdaki utility type'lar hep object'lerin property'leriyle uğraştı. Bir sonraki yazıda ikinci gruba geçiyoruz: union'ları süzen Exclude, Extract ve NonNullable, fonksiyonlardan ve class'lardan tip türeten ReturnType, Parameters, InstanceType ve Awaited.