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:
// 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 runtimeTü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ı."
// 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:
// 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:
// 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; // ✅ numberUserProfile, 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:
// 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.
// 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 timeVarsayı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.
// 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:
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:
// 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 ✅Yaygın Hatalar
Generics ile rahatladıktan sonra kaçınılması gereken birkaç kalıp:
- Generic içinde
anykullanmak.function wrap<T>(val: T): anyyazarsanız, amacı boşa çıkardınız. Tüm mesele türün akması — dönüş türü veya uygulama içindeanykullanmak, 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ç duyarkenfunction 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
stringveyanumberkabul ediyorsa ve her biri için mantık farklıysa, içinde bir tür koruyucuylafunction 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.