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

TypeScript Indexed Access Types: T["key"] ile Mevcut Tiplerden Tip Türetmek

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript Indexed Access Types: T["key"] ile Mevcut Tiplerden Tip Türetmek

T["key"] ile bir property'nin tipini okumayı, union index'leri, T[number] ile array element tipini, generic'lerde T[K]'yi ve büyük API tiplerinden parça türetmeyi örneklerle görüyoruz.

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

Önce Tahmin Et

Elimizde basit bir User type'ı var. Altındaki üç satır, bu type'ın içinden başka type'lar çekmeye çalışıyor:

type User = {
  id: number;
  name: string;
  address: { city: string; zip: string };
};

type A = User["address"]["city"];
type B = User["id" | "name"];
type C = User["email"];

Üç soru: A hangi type'a karşılık gelir? B ne olur? C satırı derlenir mi? Cevapları yazı ilerledikçe tek tek vereceğiz.

Bir Property'nin Tipini Adıyla Sormak

Daha önce gördüğümüz keyof bize bir type'ın key'lerini veriyordu. Indexed access type ise tersini yapar: bir key'i verirsin, o key'in value tipini alırsın. Yazımı JavaScript'teki köşeli parantezle property okumaya çok benzer, sadece değerlerle değil type'larla çalışır: User["name"], "User içindeki name property'sinin tipi" demektir ve sonucu string'dir.

Bunun neden işe yaradığını tekrar eden tiplerle görelim:

type Order = {
  id: string;
  status: "pending" | "paid" | "shipped";
  customer: { id: string; email: string };
};

function setStatus(order: Order, status: Order["status"]) {
  order.status = status;
}

function sendReceipt(email: Order["customer"]["email"]) {
  console.log(`Receipt sent to ${email}`);
}

sendReceipt("ada@example.com"); // Receipt sent to ada@example.com

setStatus fonksiyonunda "pending" | "paid" | "shipped" union'ını ikinci kez yazmadık; Order'a sorduk. Yarın biri bu union'a "cancelled" eklediğinde fonksiyonun parametresi de kendiliğinden genişler. Elle kopyalasaydın iki yeri birlikte güncellemeyi hatırlaman gerekirdi.

Bunu bir hesap tablosu gibi düşün. Bir hücreye başka hücredeki değeri elle yazarsan, kaynak değiştiğinde senin hücren eskide kalır. =B2 diye referans verirsen her zaman günceldir. Order["status"] de bu referanstır.

sendReceipt örneği de baştaki ilk sorunun cevabı: indexed access zincirlenebilir. User["address"] bir object type'ı verir, onun da ["city"]'sini sorabilirsin. Yani A, string'dir.

Index Olarak Union Vermek

Köşeli parantezin içine tek bir key yazmak zorunda değilsin; bir union da verebilirsin. TypeScript her key için ayrı ayrı sorar ve sonuçları birleştirir. User["id" | "name"], User["id"] | User["name"] ile aynıdır, yani number | string. İkinci sorunun cevabı bu.

Bunun en sık kullanılan hâli keyof ile birleşimidir: T[keyof T], bir type'taki bütün value tiplerinin union'ını verir.

type Settings = {
  theme: "light" | "dark";
  fontSize: number;
  autoSave: boolean;
};

type SettingValue = Settings[keyof Settings];
// "light" | "dark" | number | boolean

function updateSettingLoose(key: keyof Settings, value: SettingValue) {
  console.log(key, value);
}

updateSettingLoose("theme", 14); // derlenir ama anlamsız

Son satıra dikkat et. SettingValue doğru bir type, ama fonksiyonda key ile value arasında bir bağ yok. TypeScript'in gözünde "theme" ile 14 ayrı ayrı geçerli, birlikte anlamsız olmaları onu ilgilendirmiyor. Bu bağı kurmayı birazdan generic'lerle göreceğiz.

Index Bir Tip Olmalı

Üçüncü soruya gelelim. User içinde email diye bir property yok ve TypeScript bunu tıpkı bir değer üzerinde yanlış property okuduğundaki gibi yakalar:

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

// Hata: Property 'email' does not exist on type 'User'.
type Email = User["email"];

// Hata: Cannot access 'User.name' because 'User' is a type, but not a namespace.
type Name = User.name;

const key = "name";
// Hata: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?
type ByKey = User[key];

type ByKeyFixed = User[typeof key]; // string

Yani C satırı derlenmez, hata kodu TS2339. Bu blokta iki tuzak daha var. Birincisi nokta yazımı: tip seviyesinde User.name yazamazsın, indexed access her zaman köşeli parantezle yazılır. Derleyici bu durumda mesajın devamında doğru yazımı da önerir.

İkincisi daha önemli: köşeli parantezin içine giren şey bir değer değil, bir type'tır. "name" yazdığında aslında "name" literal type'ını veriyorsun. key ise bir değişken, yani bir değer. Değeri tip seviyesine taşımak için daha önce gördüğümüz typeof'u kullanırsın. key bir const olduğu için typeof key, "name" literal type'ıdır ve User[typeof key] de string olur.

T[number] ile Array'in Element Tipi

Array'lerin index'leri sayıdır. O hâlde bir array type'ına "number ile index'lersem ne gelir?" diye sorabilirsin. Cevap, array'in element tipidir. Bu küçük fikir, çalışma zamanında zaten var olan bir array'den type türetmenin en yaygın yolu:

const roles = ["admin", "editor", "viewer"] as const;

type Role = (typeof roles)[number];
// "admin" | "editor" | "viewer"

function canEdit(role: Role) {
  return role === "admin" || role === "editor";
}

canEdit("editor");
// Hata: Argument of type '"guest"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
canEdit("guest");

Burada iki adım var. typeof roles değeri tip seviyesine taşır ve readonly ["admin", "editor", "viewer"] tuple'ını verir. [number] da bu tuple'ın herhangi bir index'indeki element tipini, yani üç literal'in union'ını verir. Artık rol listesi tek bir yerde yaşıyor: array'e yeni bir rol eklediğinde Role type'ı da genişler. typeof roles[number] parantezsiz de aynı şekilde çalışır, ama parantezli yazım okuyana neyin önce olduğunu açıkça söyler.

Aynı yöntem object array'lerinde de işler. Elinde örnek veri varsa, type'ını ayrıca yazmadan çekebilirsin:

const products = [
  { id: 1, title: "Mechanical Keyboard", price: 120 },
  { id: 2, title: "USB-C Hub", price: 45 },
];

type Product = (typeof products)[number];
// { id: number; title: string; price: number; }

function formatPrice(product: Product) {
  return `${product.title}: $${product.price}`;
}

console.log(formatPrice(products[1])); // USB-C Hub: $45

Tuple'larda ise belirli bir pozisyonu sorabilirsin. Tuple'ın uzunluğu da bir property olduğu için ["length"] bile bir type verir:

type Entry = [key: string, value: number];

type First = Entry[0];       // string
type Size = Entry["length"]; // 2

// Hata: Tuple type 'Entry' of length '2' has no element at index '2'.
type Third = Entry[2];

Generic'lerle Birlikte: T[K]

Indexed access asıl gücünü generic'lerle birlikte gösterir. Daha önce K extends keyof T constraint'ini görmüştük. Şimdi onu T[K] ile birleştirip yukarıdaki gevşek updateSettingLoose fonksiyonunu düzeltelim:

type Settings = {
  theme: "light" | "dark";
  fontSize: number;
  autoSave: boolean;
};

const settings: Settings = { theme: "light", fontSize: 14, autoSave: true };

function updateSetting<K extends keyof Settings>(key: K, value: Settings[K]) {
  settings[key] = value;
}

function getSetting<K extends keyof Settings>(key: K): Settings[K] {
  return settings[key];
}

updateSetting("fontSize", 16);
updateSetting("theme", "dark");
// Hata: Argument of type '14' is not assignable to parameter of type '"dark" | "light"'.
updateSetting("theme", 14);

const size = getSetting("fontSize"); // number
console.log(size); // 16

Fark şurada: updateSetting("theme", ...) çağrısında TypeScript K'yi "theme" literal type'ı olarak çıkarır. Böylece value parametresinin tipi Settings["theme"], yani "light" | "dark" olur. Key ile value artık birbirine bağlı. getSetting("fontSize") da aynı mantıkla number döndürür; dönüş tipi her çağrıda verdiğin key'e göre değişir.

Hata mesajındaki sıraya dikkat: TypeScript 7 union'ları kararlı bir sırayla yazdırır, bu yüzden mesajda "dark" | "light" görürsün. Type aynı, sadece gösterim sırası farklı.

Büyük Tiplerden Parça Çıkarmak

Gerçek projelerde sık karşılaşılan bir durum: bir API'nin cevap tipi tek parça hâlinde gelir, çoğu zaman bir araçla otomatik üretilmiştir ve içindeki parçaların ayrı isimleri yoktur. Indexed access ile bu parçalara isim verebilirsin:

type OrdersResponse = {
  data: {
    orders: {
      id: string;
      total: number;
      items: { sku: string; quantity: number }[];
    }[];
  };
  meta: { page: number; pageSize: number };
};

type Order = OrdersResponse["data"]["orders"][number];
type OrderItem = Order["items"][number];
type Pagination = OrdersResponse["meta"];

function countItems(order: Order): number {
  return order.items.reduce((sum: number, item: OrderItem) => sum + item.quantity, 0);
}

function nextPage(meta: Pagination): number {
  return meta.page + 1;
}

Burada yön önemli: kaynak tip tek, diğerleri ondan türetiliyor. Cevap tipi değiştiğinde Order ve OrderItem da değişir; uyumsuz hâle gelen kod derleme hatası verir. Bir uyarı da ekleyeyim: çok uzun zincirler okunması zor type'lar üretir. Bir parça projede çok yerde kullanılıyorsa ona bu örnekteki gibi kısa bir isim ver, zinciri her seferinde baştan yazma.

Sık Düşülen Tuzaklar

type Profile = {
  name: string;
  nickname?: string;
};

type Nickname = Profile["nickname"]; // string | undefined

const statuses = ["active", "banned"];
type Status = (typeof statuses)[number]; // string

type Scores = Record<string, number>;
type Score = Scores["anyTypo"]; // number, hata vermez

Birinci tuzak optional property'ler. nickname? olduğu için Profile["nickname"] sadece string değil, string | undefined'dır. Bu type'ı bir parametreye verdiğinde fonksiyonun içinde undefined ihtimaliyle de uğraşman gerekir.

İkinci tuzak as const'u unutmak. statuses array'i as const olmadan string[] olarak çıkarılır, dolayısıyla [number] sana literal'lerin union'ını değil düz string verir. Derleyici bir şey söylemez; sadece beklediğin dar type'ı alamazsın.

Üçüncüsü index signature'lar. Record<string, number> gibi bir type her string key'i kabul ettiği için Scores["anyTypo"] hata vermez ve number döner. TypeScript'in yanlış yazılmış key'leri yakalaması, sadece key'leri tek tek bilinen type'larda işe yarar.

Bir de hatırlatma: indexed access tamamen tip seviyesinde bir işlemdir. Derlenmiş JavaScript'te Order["status"] diye bir şey kalmaz, çalışma zamanında hiçbir kontrol yapmaz.

Küçük Bir Deneme

★☆☆ Tahmin et, sonra Playground'da dene: type Config = { db: { host: string; port: number }; debug: boolean }; için Config["db"]["port"] ve Config["db" | "debug"] hangi type'lara karşılık gelir?

★★☆ Derlenir mi? const sizes = ["s", "m", "l"]; type Size = (typeof sizes)[number]; const pick: Size = "xl"; Derleniyorsa, hangi küçük değişiklikle bu satırı hataya çevirirsin?

★★★ Bir object array'inden tek bir property'nin değerlerini toplayan generic bir pluck fonksiyonu yaz: pluck(products, "title") çağrısının dönüş tipi string[], pluck(products, "price") çağrısınınki number[] olsun. İpucu: dönüş tipi T[K][].

Bu yazıda bir type'ın parçalarını tek tek çektik. Bir sonraki yazıda TypeScript'in bu tür dönüşümler için hazır sunduğu utility type'lara geçiyoruz: Partial, Pick, Omit, Record ve arkadaşları. Tanımlarına baktığında, bu yazıdaki T[K]'yi orada da göreceksin.