TypeScript yazıyor ama generics hâlâ Stack Overflow'dan tam anlamadan kopyaladığınız bir şeymiş gibi hissettiriyorsa, bu yazı tam size göre. Generics, TypeScript'i "tür ek açıklamalarına sahip JavaScript"ten gerçekten ifade gücü olan bir tür sistemine taşıyan özelliktir. Bir kez kavrandığında her yerde göreceksiniz — ve işler zorlaştığında any'ye başvurmak yerine doğal olarak bunlara yöneleceksiniz. TypeScript Handbook'un generics bölümü tam spesifikasyonu kapsar; bu makale gerçek kod tabanlarında kullanacağınız kalıplara odaklanır.

any ile İlgili Sorun

Generics belirli bir sorunu çözmek için vardır: bir fonksiyonun veya veri yapısının birden fazla türle çalışmasını istiyorsunuz, ancak bu süreçte tür bilgisini kaybetmek istemiyorsunuz. Naif çözüm any kullanmaktır — ve ne kaybettiğinizi fark edene kadar iyi görünür:

ts
// With any — TypeScript has no idea what comes out
function identity(arg: any): any {
  return arg;
}

const result = identity('hello');
// result is typed as 'any' — you've lost the string type
// TypeScript won't catch this:
result.toFixed(2); // no error at compile time, crashes at runtime

Tür bilgisi girer ama çıkmaz. result ile ne yaparsanız yapın tamamen denetlenmez. Bu bir TypeScript kod tabanı değil — ekstra adımlarla JavaScript'tir. Generics bunu şöyle diyerek çözer: "Tam türü henüz bilmiyorum, ama ne girerse aynı türle çıkmalı."

ts
// With a generic — T flows through the function
function identity<T>(arg: T): T {
  return arg;
}

const result = identity('hello');
// result is typed as 'string' ✅
result.toFixed(2); // ❌ TypeScript catches this: Property 'toFixed' does not exist on type 'string'

const count = identity(42);
// count is typed as 'number' ✅

<T> bir tür parametresi bildirir — bunu türler için bir değişken olarak düşünün. TypeScript, T'nin ne olduğunu geçirdiğiniz argümandan çıkarır; bu yüzden neredeyse hiçbir zaman açıkça yazmanız gerekmez. identity<string>('hello') çalışır, ama identity('hello') da çalışır — TypeScript bunu kendi çözer.

Pratikte Genel Fonksiyonlar

Kimlik fonksiyonu klasik bir öğretim örneğidir ama üretimde yazacağınız bir şey değildir. İşte gerçek kod tabanlarında gerçekten karşılaşılan genel fonksiyon türleri:

ts
// A type-safe array first/last helper
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const firstUser = first(users);       // UserProfile | undefined
const lastOrder = last(orders);       // Order | undefined
const firstNum  = first([1, 2, 3]);   // number | undefined

// Group array items by a key
function groupBy<T, K extends string | number>(
  items: T[],
  getKey: (item: T) => K
): Record<K, T[]> {
  return items.reduce((acc, item) => {
    const key = getKey(item);
    if (!acc[key]) acc[key] = [];
    acc[key].push(item);
    return acc;
  }, {} as Record<K, T[]>);
}

// TypeScript infers T as Order and K as string
const byStatus = groupBy(orders, order => order.status);
// byStatus: Record<string, Order[]>
// byStatus['pending'] is Order[] ✅

TypeScript'in T ve K'yı fonksiyonu nasıl çağırdığınızdan otomatik olarak çıkardığına dikkat edin. Çağrı noktasında tek bir açık tür ek açıklaması yazmadan dönüş değerinde tam tür güvenliği elde edersiniz. İşte asıl kazanım bu.

Genel Arayüzler ve Türler

Generics'in günlük iş için vazgeçilmez hale geldiği yer burasıdır. Bir API ile konuşan her kod tabanı, sonunda birkaç genel wrapper türüne ihtiyaç duyar. İşte sürekli göreceğiniz (ve yazacağınız) türler:

ts
// API response wrapper — used across every endpoint
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

// Paginated list response
interface PaginatedResult<T> {
  items: T[];
  page: number;
  pageSize: number;
  totalItems: number;
  totalPages: number;
}

// Using them with concrete types
interface UserProfile {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
}

interface Order {
  id: number;
  userId: number;
  total: number;
  status: 'pending' | 'shipped' | 'delivered';
}

// The return types are fully typed — no casting needed downstream
async function getUser(id: number): Promise<ApiResponse<UserProfile>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

async function listOrders(page: number): Promise<PaginatedResult<Order>> {
  const res = await fetch(`/api/orders?page=${page}`);
  return res.json();
}

const response = await getUser(1);
response.data.email;        // ✅ string
response.data.avatarUrl;    // ✅ string

const result = await listOrders(1);
result.items[0].status;     // ✅ 'pending' | 'shipped' | 'delivered'
result.totalPages;          // ✅ number
Bu türleri gerçek API yüklerinden oluştururken, JSON to TypeScript aracı iç türleri (UserProfile, Order) örnek bir yanıttan otomatik olarak oluşturabilir. Sonra bunları genel wrapper'larınıza bağlarsınız.

Genel Kısıtlamalar

Bazen birden fazla türü kabul etmek istersiniz, ancak kesinlikle her türü değil. Genel kısıtlamalar, bir tür parametresinin sahip olması gerekenleri belirtmenizi sağlar. Sözdizimi T extends SomeType şeklindedir — "T, SomeType'a atanabilir olmalıdır" anlamına gelir:

ts
// Without constraint — TypeScript doesn't know T has an id property
function findById<T>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id); // ❌ Property 'id' does not exist on type 'T'
}

// With constraint — T must have at least an id: number field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id); // ✅
}

// Works with any type that has id: number
const user  = findById(users, 42);   // UserProfile | undefined
const order = findById(orders, 101); // Order | undefined

// Another common constraint — T must be an object (excludes primitives)
function mergeDefaults<T extends object>(partial: Partial<T>, defaults: T): T {
  return { ...defaults, ...partial };
}

// T must have a name: string and email: string
function formatContact<T extends { name: string; email: string }>(contact: T): string {
  return `${contact.name} <${contact.email}>`;
}

// Works on any object with those two fields — UserProfile, Employee, whatever
formatContact({ name: 'Alice', email: '[email protected]', role: 'admin' }); // ✅

Kısıtlama, T'yi tam olarak { id: number } ile kilitlemez — T'nin en azından o şekle sahip olması gerektiği anlamına gelir. Bu nedenle on alanlı tam bir UserProfile geçirmek sorun değildir. Bu, TypeScript'in en güçlü ve iyi belgelenmiş özelliklerinden biri olan yapısal yazımın pratikte nasıl işlediğidir.

keyof Kısıtlaması

TypeScript standart kütüphanesinde ve gerçek kod tabanlarında en kullanışlı genel kalıplardan biri, generics'i keyof ile birleştirmektir. Bu, bir nesne türünün özellik adını kabul eden ve dönüş türünün o özellikle eşleşeceğini garanti eden fonksiyonlar yazmanızı sağlar.

ts
// keyof T is the union of all keys of T as string literals
// K extends keyof T means K must be one of those keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: UserProfile = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  avatarUrl: 'https://example.com/avatar.png'
};

const name  = getProperty(user, 'name');  // string ✅
const id    = getProperty(user, 'id');    // number ✅
const email = getProperty(user, 'email'); // string ✅

getProperty(user, 'password'); // ❌ Argument of type '"password"' is not assignable to
                               //    parameter of type 'keyof UserProfile'

// Real use case: a generic sort function
function sortBy<T>(items: T[], key: keyof T): T[] {
  return [...items].sort((a, b) => {
    const av = a[key];
    const bv = b[key];
    return av < bv ? -1 : av > bv ? 1 : 0;
  });
}

const byName = sortBy(users, 'name');   // ✅ sorted by name
const byId   = sortBy(users, 'id');     // ✅ sorted by id
sortBy(users, 'nonexistent');           // ❌ caught at compile time

Varsayılan Tür Parametreleri

Fonksiyon parametrelerinin varsayılanları olabildiği gibi, tür parametrelerinin de varsayılanları olabilir. Bu, neredeyse her zaman belirli bir türle kullanılan genel bir türünüz olduğunda kullanışlıdır, ancak olmadığı durumlar için esnekliği korumak istiyorsunuzdur.

ts
// Default type parameter — T defaults to string if not specified
interface Cache<T = string> {
  get(key: string): T | undefined;
  set(key: string, value: T, ttlMs?: number): void;
  delete(key: string): void;
  clear(): void;
}

// Without specifying T — uses the default (string)
declare const stringCache: Cache;
const val = stringCache.get('theme'); // string | undefined

// Specifying a different T
declare const userCache: Cache<UserProfile>;
const user2 = userCache.get('user:42'); // UserProfile | undefined

// Another useful example: an event emitter with a typed payload
interface TypedEvent<TPayload = void> {
  subscribe(handler: (payload: TPayload) => void): () => void;
  emit(payload: TPayload): void;
}

// Events with no payload — default void keeps the API clean
const appReady: TypedEvent = { /* ... */ };
appReady.emit(); // no argument needed

// Events with a payload
const userLoggedIn: TypedEvent<{ userId: number; timestamp: Date }> = { /* ... */ };
userLoggedIn.emit({ userId: 42, timestamp: new Date() });

Yerleşik Genel Yardımcı Programlar

TypeScript, yukarıda öğrendiğiniz generics kullanılarak uygulanmış bir dizi yerleşik yardımcı tür ile gelir. Bunları nasıl kullanacağınızı anlamak, sahip olabileceğiniz en pratik TypeScript becerilerinden biridir. İşte çıktı türünü gösteren gerçek örneklerle temel set:

ts
interface UserProfile {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
  passwordHash: string;
  createdAt: Date;
}

// Partial<T> — all fields become optional (great for PATCH payloads)
type UpdateUserPayload = Partial<UserProfile>;
// { id?: number; name?: string; email?: string; role?: ...; ... }

// Required<T> — all fields become required (reverse of Partial)
interface DraftConfig {
  apiUrl?: string;
  timeout?: number;
  maxRetries?: number;
}
type ResolvedConfig = Required<DraftConfig>;
// { apiUrl: string; timeout: number; maxRetries: number }

// Pick<T, K> — keep only the named fields
type UserSummary = Pick<UserProfile, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Use in list views — only ship what the UI needs

// Omit<T, K> — exclude the named fields
type PublicUserProfile = Omit<UserProfile, 'passwordHash'>;
// { id: number; name: string; email: string; role: ...; createdAt: Date }
// Safe to include in API responses

// Record<K, V> — typed dictionary / map
type PermissionMap = Record<UserProfile['role'], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }

const permissions: PermissionMap = {
  admin:  ['read', 'write', 'delete', 'admin'],
  user:   ['read', 'write'],
  viewer: ['read']
};

// ReturnType<T> — infer what a function returns (keeps types in sync automatically)
function buildUserSession(user: UserProfile) {
  return {
    token: crypto.randomUUID(),
    userId: user.id,
    role: user.role,
    expiresAt: new Date(Date.now() + 3_600_000)
  };
}

type UserSession = ReturnType<typeof buildUserSession>;
// { token: string; userId: number; role: 'admin' | 'user' | 'viewer'; expiresAt: Date }
// Change buildUserSession and UserSession updates automatically ✅

Mevcut JavaScript'i TypeScript'e dönüştürüyor ve bu türleri sıfırdan oluşturuyorsanız, JS to TypeScript aracı ilk türleri yerleştirmenize yardımcı olabilir, böylece üzerlerine yardımcı türler eklemeye başlayabilirsiniz.

Gerçekçi Uçtan Uca Örnek: Türlendirilmiş API İstemcisi

Türlendirilmiş bir API katmanı oluştururken her seferinde başvurduğum kalıp budur. Genel fonksiyonları, genel arayüzleri ve yardımcı türleri, fetch çağrısından veriyi tüketen bileşene kadar uçtan uca tür güvenliği sağlayan bir yapıya birleştirir:

ts
// The response envelope — wraps every API response
interface ApiResponse<T> {
  data: T;
  meta: {
    requestId: string;
    duration: number;
  };
}

// Error envelope — what the API sends on failure
interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

// The generic fetch wrapper
async function fetchApi<T>(
  url: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  const res = await fetch(url, {
    headers: { 'Content-Type': 'application/json', ...options?.headers },
    ...options
  });

  if (!res.ok) {
    const err: ApiError = await res.json();
    throw new Error(`[${err.code}] ${err.message}`);
  }

  return res.json();
}

// Typed endpoint functions — T is set at each call site
async function getUser(id: number): Promise<UserProfile> {
  const { data } = await fetchApi<UserProfile>(`/api/users/${id}`);
  return data;
}

async function listOrders(userId: number, page = 1): Promise<PaginatedResult<Order>> {
  const { data } = await fetchApi<PaginatedResult<Order>>(
    `/api/users/${userId}/orders?page=${page}`
  );
  return data;
}

async function createOrder(
  payload: Pick<Order, 'userId' | 'total'>
): Promise<Order> {
  const { data } = await fetchApi<Order>('/api/orders', {
    method: 'POST',
    body: JSON.stringify(payload)
  });
  return data;
}

// Usage — fully typed, no casting anywhere
const user = await getUser(42);
console.log(user.email);          // string ✅

const orders = await listOrders(42);
orders.items[0].status;           // 'pending' | 'shipped' | 'delivered' ✅
orders.totalPages;                // number ✅

const newOrder = await createOrder({ userId: 42, total: 99.99 });
newOrder.id;                      // number ✅
Bu kalıpların herhangi birini doğrudan TypeScript Playground'da deneyebilirsiniz — kodu yapıştırın, çıkarılan türleri görmek için değişkenlerin üzerine gelin ve derleyicinin ne yakaladığını görmek için bir şeyleri bozmayı deneyin. Generics öğrenmek için en hızlı geri bildirim döngüsüdür.

Yaygın Hatalar

Generics ile rahatladıktan sonra kaçınılması gereken birkaç kalıp:

  • Generic içinde any kullanmak. function wrap<T>(val: T): any yazarsanız, amacı boşa çıkardınız. Tüm mesele türün akması — dönüş türü veya uygulama içinde any kullanmak, TypeScript'in artık takip edemeyeceği anlamına gelir.
  • Belirli türlerle aşırı kısıtlamak. Yalnızca T extends { id: number }'a ihtiyaç duyarken function process<T extends UserProfile> yazmak çok kısıtlayıcıdır. Uygulamanızın çalışması için gereken minimum kısıtlamayı kullanın — bu şekilde fonksiyon aynı şekle sahip olan farklı türler için yeniden kullanılabilir kalır.
  • Bir birleşimin yeteceği yerde generics'e başvurmak. Bir fonksiyon string veya number kabul ediyorsa ve her biri için mantık farklıysa, içinde bir tür koruyucuyla function f(arg: string | number) generics'ten daha temizdir. Generics, tüm tür varyantlarına aynı mantık uygulandığında parlar.
  • Çok fazla tür parametresi. <T, U, V, W> yazdığınızı fark ederseniz geri adım atın. Bu genellikle fonksiyonun çok fazla şey yaptığının veya türlerin tek bir arayüz olarak ifade edilebileceğinin işaretidir. TypeScript derleyici kaynak kodunun kendisi iyi bir referanstır — karmaşık yardımcı programlar bile nadiren 2–3'ten fazla tür parametresine ihtiyaç duyar.

Özet

Generics, TypeScript'in "açıklamalı JavaScript" olmaktan çıkıp gerçekten güçlü bir tür sistemi olmaya başladığı noktadır. Temel fikir basittir: türü bir parametre olarak yakalayın, böylece bilgi kaybetmeden fonksiyonunuz veya arayüzünüz üzerinden akabilsin. Oradan kısıtlamalar, kabul edilebilir olanı daraltmanıza izin verir, keyof size tür güvenli özellik erişimi sağlar ve yerleşik yardımcı türler (Partial, Pick, Omit, Record) aksi halde elle yazacağınız kalıpları yönetir. En iyi sonraki adım gerçek bir proje açmak, başka bir yol bilmediğiniz için any kullandığınız bir fonksiyon bulmak ve bunu bir generic ile değiştirmektir. Generics hakkındaki TypeScript Handbook ve yardımcı tür referansı yer imlerine değer iki sayfadır. Ve gerçek API yüklerinden arayüzler oluşturuyorsanız, JSON to TypeScript somut türleri oluşturabilir — sonra bunları tam uçtan uca tür güvenliği için ApiResponse<T> ve PaginatedResult<T> gibi genel yardımcı türlerinize sarmanız yeterlidir.