Frontend

TypeScript ile Enterprise Kod: Tip Güvenliği ve Geliştirici Deneyimi

Ahmet BulutAhmet Bulut
12 Ocak 2026
12 dk

Geçen ay bir startup'ın koduna baktım. 40 bin satır JavaScript, 3 yıllık birikmiş borç, ve product owner şunu söylüyor: "Yeni feature eklediğimizde 4-5 yerde bir şey kırılıyor, sebebini kimse bilmiyor." Klasik hikâye. Sebep de ortada aslında: user.profile.name dediğinde profile'ın null olabileceğini kimse hatırlamıyor, çünkü bunu söyleyen bir mekanizma yok. Sadece yorum satırları var, o da sadece bazı yerlerde.

İşte TypeScript bunun için var. Sihirli bir performans aracı değil, runtime'da hiçbir şey değiştirmiyor. Yaptığı tek şey, sen kod yazarken "bu değişken bazen undefined olabilir, dikkat et" demek. Kulağa basit geliyor ama 50+ kişinin koda dokunduğu bir kod tabanında bu uyarı, gönderdiğin pull request'lerin yarısının daha merge edilmeden hatasını yakalıyor.

TypeScript neden var, gerçekte ne çözüyor

JavaScript'in en sevdiğim ve en nefret ettiğim özelliği aynı: esnekliği. const user = {} yazıyorsun, sonra user.name = "Ahmet", sonra user.name = 42, kimse itiraz etmiyor. Tek başına çalışırken harika. 8 kişilik bir ekipte 2 yıl sonra cehennem.

TypeScript, JavaScript'e yapısal tip sistemi ekliyor. Yani "bu fonksiyon bir User alıyor" dediğinde, sana User'ın ne olduğunu hatırlatmıyor sadece, geçirdiğin objenin gerçekten User şekline uyduğunu da kontrol ediyor. Bu kontrol derleme zamanında oluyor. Production'a çıktığında ortada TypeScript falan kalmıyor, sıradan JavaScript çalışıyor.

Pratikte ne çözüyor? Üç şey: refactor güvenliği, otomatik dokümantasyon, IDE'nin sana akıllı öneriler sunabilmesi. Üçü de pazarlama dili gibi geliyor olabilir. Somutlaştırayım: 200 dosyalık bir projede bir field'ın adını user_name'den username'e değiştireceksin. JavaScript'te bu, find-replace ve dua. TypeScript'te derleyici sana bunu unuttuğun 23 yeri gösteriyor.

tsconfig.json: strict modu kapatma alışkanlığı

TypeScript projelerinde gördüğüm en yaygın hata, tsconfig.json'da "strict": false bırakmak veya hiç açmamak. Bu, spor salonuna abonelik almak ama hiç gitmemek gibi. Tipler var ama hiçbir şeyi zorunlu kılmıyorsun. Sonuçta her yere any atılıyor, sen de "TypeScript zaten bir şey yakalamıyor" diye sızlanıyorsun.

Yeni bir projeye başlıyorsan strict mode bir tartışma konusu olmamalı:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

strict: true aslında bir bayraklar paketi: strictNullChecks, noImplicitAny, strictFunctionTypes ve diğerleri. En kritik olan strictNullChecks, çünkü "milyar dolarlık hata" dediğimiz şeyi (null reference) compile time'a taşıyor.

noUncheckedIndexedAccess'i ayrıca öneriyorum. Default davranış şu: const arr: string[] = []; const first = arr[0]; derleyici firststring olarak kabul ediyor. Halbuki dizi boş, gerçekte undefined. Bu bayrak açıkken first otomatik olarak string | undefined oluyor. Sinir bozucu mu? İlk hafta evet. Sonra hayatını kurtarıyor.

Günlük olarak kullanacağın tipler

TypeScript'in dokümantasyonunu açıp utility type listesine bakınca insanın gözü kararıyor. Gerçek hayatta her gün 5-6 tanesini kullanıyorsun, gerisi nadiren işine yarıyor. İşte favorilerim:

Partial: Tüm field'ları opsiyonel yapar. Update endpoint'lerinde harika. Pick: Sadece istediğin field'ları seçer. Omit: İstemediklerini çıkarır, yeni bir tip döner. API response'unu temizlerken çok işe yarıyor. Record: Anahtar-değer haritaları için.

interface User {
  id: string;
  email: string;
  password: string;
  createdAt: Date;
}

// API'ye dönerken password'ü göndermek istemiyoruz type PublicUser = Omit;

// Update fonksiyonunda her field opsiyonel type UserUpdate = Partial>;

function updateUser(id: string, patch: UserUpdate) { // patch.email opsiyonel, patch.password opsiyonel }

ReturnType ve Awaited de cebimde tuttuğum iki silah. Özellikle Awaited> gibi zincirler API katmanında durmadan ortaya çıkıyor.

type mı interface mı: bitmeyen kavga

Bu konuda Twitter'da takım tutuluyor neredeyse. Pratik cevap çok daha sıkıcı: ikisi de çalışıyor, %95 durumda fark etmiyor. Yine de benim kuralım şu:

Object şekilleri için interface kullan. Çünkü interface'ler aynı isimle birden fazla tanımlandığında otomatik birleşiyor (declaration merging). Üçüncü parti library'leri extend ederken hayat kurtarıyor. Üstelik error mesajlarında interface ismi olduğu gibi kalıyor, type bazen genişleyip okunmaz hale geliyor.

Union, intersection, mapped type, conditional type yazıyorsan type kullan. interface bunları yapamıyor zaten.

// interface: bir nesne şekli
interface Project {
  id: string;
  name: string;
  ownerId: string;
}

// type: union yazılırken zorunlu type TaskStatus = "todo" | "in_progress" | "done"; type TaskWithMeta = Task & { meta: Record };

Tek tutarlı kural: takım içinde bir karar al, ekipteki herkes aynı şeyi yapsın. Ben yıllardır interface tarafındayım ama bu dini bir tercih değil, sadece error mesajları daha temiz çıkıyor.

Generics: korkutucu görünen ama aslında basit

İlk gördüğümde "function fetch(url: string): Promise bu da neyin nesi" demiştim. Bir hafta sonra bensiz yapamadığım şeye dönüştü. Generic'in olayı şu: bir fonksiyon yazıyorsun ama tipini sen değil, çağıran kişi belirleyecek. Yani "ne tipinde olduğunu sonra söylerim" demenin yolu.

// Generic olmadan: her endpoint için ayrı fonksiyon
function fetchUser(id: string): Promise { / ... / }
function fetchProject(id: string): Promise { / ... / }

// Generic ile: tek fonksiyon, çağıran tipini söyler async function fetchById(endpoint: string, id: string): Promise { const res = await fetch(/${endpoint}/${id}); return res.json() as T; }

const user = await fetchById("users", "abc"); const project = await fetchById("projects", "xyz");

Generic constraint'leri de unutma. dediğinde, T ne olursa olsun en azından bir id field'ı olacağını garanti edersin. Bu tarz kısıtlar, generic'i "her şey olabilir"den "en azından şuna uyan bir şey" haline getiriyor.

Yeni başlayanlara tavsiyem: ilk 3 ayda generic yazmaya çalışma, kullanmaya çalış. Library'lerin generic'lerini doğru çağırmayı öğrendiğinde kendiniinkileri yazmak doğal geliyor.

Discriminated unions: bence TS'in en güçlü silahı

Bu konuyu anlatırken her seferinde aynı örneği veriyorum çünkü çok temiz bir kazanım sağlıyor. Bir API response'unu modelliyorsun. Üç durum var: yükleniyor, başarılı, hatalı. Naif yaklaşım:

interface State {
  loading: boolean;
  data?: User;
  error?: string;
}

Sorun: loading: true ama data dolu olabilir mi? error ve data aynı anda dolu olur mu? Kod, geçersiz state'lere izin veriyor, sen de her ihtimale karşı if'lerle savunma yapmak zorunda kalıyorsun.

Discriminated union ile aynı şeyi modelle:

type State =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; message: string };

function render(state: State) { switch (state.status) { case "loading": return "Yükleniyor..."; case "success": return state.data.email; // TS biliyor, data burada var case "error": return state.message; // message burada var } }

Compiler, state.status kontrolüne göre doğru field'ları bilir. Geçersiz state'leri ifade etmek imkansız hale gelir. Birden fazla async kaynağın koreografisini yöneten ekran tasarlıyorsan bu pattern'i öğrenmek tek başına TS'i hak ediyor.

Görev yönetimi yaparken kullandığım Poitim'in görev modülünde bu pattern her yerde. Görev statüsü 8 farklı durum olabiliyor ve her durumda farklı field'lar zorunlu. Discriminated union olmadan bunu sürdürülebilir tutmak çok zor.

any, unknown, never: tehlikeli üçlü

Bu üç tip yanlış kullanılıyor diye TS'in sunduğu tüm güvenlik bir anda buharlaşabiliyor. Tek tek bakalım.

any: tip kontrolünü kapatma anahtarı. any kullandığında "bu değişkenle istediğimi yaparım, kontrol etme" demiş oluyorsun. Sorun şu ki, any'lik bulaşıcı: any bir değişkenden gelen her şey otomatik any oluyor. Tek bir any 30 dosyaya yayılabiliyor. Kullanmamaya çalış. Mecburen kullanırken yanına // eslint-disable-next-line ve niye yaptığını açıklayan bir yorum koy.

unknown: any'nin akıllı kuzeni. "Bilmiyorum bu ne, ama bilmediğimi de biliyorum" demek. unknown bir değişkene direkt erişemezsin, önce tip kontrolü yapmak zorundasın. JSON.parse sonucu, harici API response'u, localStorage'dan gelen değer — hepsi unknown olmalı.

function parsePayload(raw: string): User {
  const parsed: unknown = JSON.parse(raw);
  if (
    typeof parsed === "object" &&
    parsed !== null &&
    "email" in parsed &&
    typeof parsed.email === "string"
  ) {
    return parsed as User;
  }
  throw new Error("Invalid payload");
}

Daha temizi: zod, valibot gibi runtime validation kütüphaneleri. unknown'dan User'a geçiş yaparken validation'ı atlama.

never: "bu kod hiçbir zaman çalışmamalı" demek. switch'in default case'inde exhaustiveness kontrolü için altın değerinde:

function handle(status: TaskStatus) {
  switch (status) {
    case "todo": return "...";
    case "in_progress": return "...";
    case "done": return "...";
    default:
      const _exhaustive: never = status;
      throw new Error(Unhandled: ${_exhaustive});
  }
}

TaskStatus'a yeni bir değer eklediğinde compiler default case'i kıracak. Refactor sırasında kapanmamış case'leri yakalamanın en güvenilir yolu.

Mevcut JS projesini taşımak: kademeli plan

"Pazartesi sabahı bütün .js dosyalarını .ts yapalım" diyen kişi ya çok cesur ya da koddan haberi yok. Gerçek bir kod tabanını TS'e taşımak haftalar veya aylar sürer. Ben şu sırayı takip ediyorum:

1. Hazırlık. typescript, @types/node ve kullandığın framework'ün tip paketlerini ekle. tsconfig.json'ı oluştur ama strict: false ile başla, allowJs: true ve checkJs: false aç. Hedef: kodun şu anki haliyle build olabilsin.

2. JSDoc dönemi (opsiyonel). Kritik fonksiyonlara JSDoc tipler eklemeye başla. @param ve @returns ile yazılan tipler TS tarafından okunuyor. Henüz dosya uzantısı değişmeden bile compiler hata göstermeye başlıyor.

3. Yaprak dosyalardan başla. Yardımcı fonksiyonlar, sabitler, küçük utility'ler. Bunları .ts'e çevir. Kimse bu dosyalara bağımlı olmadığı için patlama riski düşük.

4. Domain modelleri. User, Project, Task gibi temel tiplerini bir types/ klasöründe oluştur. Bunları kademeli olarak fonksiyonlara enjekte et.

5. Ucu açık dosyaları en sona bırak. 2000 satırlık utils.js dosyasını en sonra çevirirsin. Çünkü her yere bağlı, etkisi büyük.

6. Strict modu açma günü. Her şey .ts olunca strict: true yap, sonra fix et. Aşamalı olarak strictNullChecks'tan başlayıp diğer flag'leri açabilirsin de. Genelde 2-3 binin altında hata kalıyor, bir hafta içinde temizlenebiliyor.

Migration sırasında ekipteki geliştirici akışını organize ederken bir şey de söyleyeyim: TS'e geçiş feature freeze gerektirmiyor. Feature geliştirmeye paralel ilerlemeli. Yoksa hiçbir zaman bitmez, çünkü kod günlük olarak büyümeye devam ediyor.

Yeni başlayanların düştüğü çukurlar

Junior bir geliştiriciye TS öğretirken hep aynı hataları görüyorum. Listeleyeyim ki sen yapma.

any'yi sığınak olarak kullanmak. Tip hatasıyla karşılaşıp 5 dakika düşündükten sonra : any yazıp kaçmak. O 5 dakikada öğrenecektin. any yazdığında zaman kazanıyorsun ama gelecekteki sen, geçmişteki senden nefret edecek.

Tipi her yere yazmak. const name: string = "Ahmet" gereksiz, TS zaten name'in string olduğunu çıkarabiliyor. Sadece fonksiyon parametrelerinde, return tiplerinde (özellikle public API'lerde) ve değişken atanmadığında tip yaz. Geri kalanını inference'a bırak.

Type assertion'ı (as Foo) doğrulama olarak kullanmak. as sadece compiler'a yalan söylüyor, runtime'da bir şey doğrulamıyor. response.data as User dediğinde data gerçekten User olmayabilir. Validation kütüphanesi kullan veya manuel type guard yaz.

Her şeyi enum yapmak. TS'in enum'u tartışmalı bir özellik, runtime'da nesne üretiyor ve tree-shake edilemiyor. Modern stack'te const assertion ve union type tercih edilir:

// enum yerine
const TaskStatus = {
  Todo: "todo",
  InProgress: "in_progress",
  Done: "done",
} as const;

type TaskStatus = typeof TaskStatus[keyof typeof TaskStatus];

Editör hatasını görmezden gelmek. Kırmızı çizgili kodu "çalışıyor zaten" diye merge etmek, geleceğin teknik borcu. TS'in sana söylediği her şey bir nedeni var. Anlaşılmıyorsa açıklamasını okumaya 30 saniye ayır.

Generic'i kullanmadan kopyalamak. fetchUser, fetchProject, fetchTask diye üç ayrı fonksiyon yazmak yerine bir fetchById yazmak. DRY prensibi tip seviyesinde de geçerli.

TypeScript bir araç. İyi araçlar gibi, başlangıçta yavaşlatıyor, sonra hızlandırıyor. İlk projende belki 2 hafta verim düşer. Üçüncü projende kodu TS olmadan yazmak hayal bile edilemez gelir. Eğer takımının projelerini ciddi olarak büyütmeyi planlıyorsan, bu yatırımı yapmamak için kötü bir gerekçen olması lazım.

Son bir şey: TypeScript cennet vaat etmiyor. Tipler yanlış da yazılabiliyor, kod hâlâ runtime'da patlayabiliyor, kötü mimari TS'le düzelmiyor. Ama doğru kullanıldığında, yazdığın kodun ömrünü 10 kat uzatıyor. Üstelik artık bu seçim, ciddi bir frontend ekibi için tartışılır olmaktan çıktı.

Sık Sorulan Sorular

TypeScript Rehberi: Tip Güvenliği ve Enterprise Kod İçin Pratik