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

JavaScript'ten TypeScript'e Geçiş: checkJs, JSDoc ve Kademeli strict

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
JavaScript'ten TypeScript'e Geçiş: checkJs, JSDoc ve Kademeli strict

Bir JavaScript projesini TypeScript'e kademeli geçirmeyi görüyoruz: allowJs, checkJs ve // @ts-check, JSDoc ile tip yazmak, TypeScript 7'deki JSDoc değişiklikleri, kademeli strict ve @ts-expect-error.

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

Önce Tahmin Et

Bu bir TypeScript dosyası değil, düz bir cart.js. Tek fark en üstteki yorum satırı ve fonksiyonun üstündeki JSDoc bloğu:

// @ts-check

/**
 * @param {number} price
 * @param {number} quantity
 * @returns {number}
 */
function lineTotal(price, quantity) {
  return price * quantity;
}

const input = document.querySelector("input");
const total = lineTotal(input.value, 2);
console.log(total.toFixed(2));

Bu dosyayı tsc'ye verirsen hata alır mısın? Alırsan kaç tane ve hangi satırda? Bir ipucu: dosyada tek bir TypeScript syntax'ı yok. Cevabı, JavaScript dosyalarının nasıl denetlendiğini gördükten sonra vereceğiz.

Neden Kademeli Geçiş?

Elinde yıllardır çalışan, yüzlerce dosyalık bir JavaScript projesi var ve TypeScript'e geçmek istiyorsun. İlk akla gelen, bütün dosyaların uzantısını .ts yapıp hataları düzeltmek. Bunu küçük bir projede yapabilirsin, ama gerçek bir projede binlerce hatayla karşılaşırsın ve proje haftalarca yarım kalır. Bu sırada ekibin geri kalanı yeni özellik yazmaya devam eder ve sen sürekli çakışma çözersin.

TypeScript'in superset olması burada işimize yarıyor. JavaScript ve TypeScript dosyaları aynı projede yan yana yaşayabilir, birbirini import edebilir. Tip denetimini dosya dosya açabilir, strict ayarlarını tek tek sıkılaştırabilirsin. Uygulama her adımda çalışmaya devam eder. Bu yazıda bu yolun adımlarını sırasıyla göreceğiz.

İlk Adım: allowJs ve checkJs

tsc varsayılan olarak .js dosyalarına dokunmaz. Baştaki dosyayı ayar vermeden derlersen TS6504 alırsın ve derleyici allowJs'i açmanı önerir. Geçişin ilk tsconfig'i şuna benzer:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "strict": true,
    "noImplicitAny": false
  },
  "include": ["src"]
}

allowJs, JavaScript dosyalarını projenin parçası yapar: .ts dosyaların onları import edebilir ve TypeScript onların type'larını koddan çıkarır. checkJs ise JavaScript dosyalarında da hata raporlanıp raporlanmayacağını belirler. Başta kapalı tutuyoruz; aksi hâlde ilk gün yüzlerce hata görürsün. Tek tek dosyaları denetime açmak için dosyanın başına // @ts-check yazarsın. Tersine, checkJs'i sonradan açtığında henüz hazır olmayan bir dosyayı // @ts-nocheck ile dışarıda bırakabilirsin. strict ve noImplicitAny satırlarına birazdan döneceğiz.

Bu küçük projeyi npx tsc -p tsconfig.json ile denediğimizde // @ts-check yazmadığımız JavaScript dosyasında, bariz bir hata içermesine rağmen tek satır uyarı çıkmadı; // @ts-check olan dosyadaki yanlış argüman ise TS2345 ile yakalandı.

Şimdi baştaki sorunun cevabı. --allowJs ile denetlediğimizde 13. satırda iki hata çıkıyor. İlki TS18047, "'input' is possibly 'null'.": querySelector eşleşen bir element bulamazsa null döndürür ve strict mod bunu biliyor. İkincisi TS2345: input.value bir string, ama JSDoc'ta price'ın number olduğunu söyledik. Tek bir TypeScript syntax'ı yazmadan, sadece yorumlarla, Temel serinin başında gördüğümüz hatanın aynısını yakaladık.

JSDoc ile Tip Yazmak

JSDoc, JavaScript dünyasında uzun zamandır kullanılan bir dokümantasyon formatı. TypeScript bu yorumları okuyup tip bilgisi olarak kullanıyor. Yani bir dosyayı .ts'e çevirmeden de oldukça zengin tipler yazabilirsin:

// @ts-check
/** @import { Product } from "./types.ts" */

/**
 * @typedef {object} CartItem
 * @property {Product} product
 * @property {number} quantity
 */

/** @type {CartItem[]} */
const items = [];

/**
 * @param {Product} product
 * @param {number} [quantity]
 */
export function addToCart(product, quantity = 1) {
  items.push({ product, quantity });
}

/**
 * @template T
 * @param {T[]} list
 * @returns {T | undefined}
 */
export function last(list) {
  return list[list.length - 1];
}

addToCart({ id: "p1", name: "Mechanical Keyboard", price: 120 });
const lastItem = last(items);
console.log(lastItem?.product.name);
addToCart({ id: "p2", name: "Mouse" }); // Hata

Burada types.ts, içinde id, name ve price olan bir Product interface'i export eden küçük bir TypeScript dosyası. Parçalara bakalım. TS 5.5 ile gelen @import, bir type'ı çalışma zamanında hiçbir şey import etmeden JSDoc'a getiriyor; import type'ın JSDoc karşılığı. @typedef ve @property bir object type tanımlıyor, interface yazmak gibi. @type bir değişkene annotation ekliyor. Köşeli parantez içindeki [quantity] parametreyi opsiyonel yapıyor. @template da generic'lerin JSDoc'taki adı: last(items)'ın dönüş tipi CartItem | undefined olarak çıkarılıyor.

Son satırda price eksik ve tsc bunu TS2741, "Property 'price' is missing in type '{ id: string; name: string; }' but required in type 'Product'." ile yakalıyor. JSDoc'un bir yan faydası da var: yazdığın tipler aynı zamanda dokümantasyon. Editör bir fonksiyonun üstüne geldiğinde bu açıklamaları gösteriyor.

TypeScript 7'de JSDoc Değişiklikleri

TypeScript 7 yeni dil özelliği getirmedi, ama JavaScript dosyalarının denetimini yeniden düzenledi. Eski projelerde sık görülen iki kalıp artık çalışmıyor: @enum ve Closure tarzı fonksiyon type'ları. Önce @enum:

// @ts-check
/** @enum {string} */
const Status = {
  Active: "active",
  Archived: "archived",
};

/** @param {Status} status */
function show(status) {
  console.log(status);
}
show("active");

TypeScript 7.0.2 bunu kabul etmiyor:

error TS2749: 'Status' refers to a value, but is being used as a type here. Did you mean 'typeof Status'?

Bu, 7.0'daki genel bir kuralın parçası: JSDoc'ta bir değer artık type gibi kullanılamıyor. Çözüm, değerleri ve type'ı ayırmak; örneğin @typedef {"active" | "archived"} Status ile bir union tanımlamak ve object'e başka bir isim vermek. Closure tarzı {function(string): number} yazımı da artık bir syntax hatası (TS1005); yerine TypeScript'in kendi yazımını kullan: {(text: string) => number}. Eski JSDoc'lu bir projeyi TypeScript 7'ye taşırken ilk karşılaşacağın hatalar büyük ihtimalle bunlar olacak.

Dosyaları .ts'e Çevirmek

Bir dosya // @ts-check ile temiz hâle geldiyse onu .ts'e çevirmek artık küçük bir iş. Sıra önemli: başka hiçbir dosyaya bağımlı olmayan "yaprak" dosyalardan, yani yardımcı fonksiyonlardan ve sabitlerden başla. Onlar tiplendikçe onları kullanan dosyalar da bedavaya tip kazanır. Yukarıdaki JSDoc'lu dosyanın .ts hâli şöyle:

interface Product {
  id: string;
  name: string;
  price: number;
}

interface CartItem {
  product: Product;
  quantity: number;
}

const items: CartItem[] = [];

export function addToCart(product: Product, quantity = 1) {
  items.push({ product, quantity });
}

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

JSDoc etiketleri birebir TypeScript syntax'ına dönüşüyor: @typedef bir interface'e, @template T <T>'ye, [quantity] varsayılan değerli bir parametreye. Bir uyarı: .ts dosyalarında JSDoc'taki tipler kullanılmaz. Dosyayı çevirip @param {number} x yorumunu bırakırsan x yine tipsiz kalır ve strict modda TS7006, "Parameter 'x' implicitly has an 'any' type." alırsın. Çevirirken tipleri yorumdan koda taşı.

Kademeli strict

TS 6.0'dan beri strict varsayılan olarak açık ve yeni bir projede bunu kapatmak için hiçbir sebep yok. Eski bir projede ise strict ailesinin hepsini bir anda açmak, geçişin en çok hata üreten anı olur. Pratik yol, strict'i açık bırakıp en gürültülü bir ya da iki üyesini geçici olarak kapatmak. Yukarıdaki tsconfig'de noImplicitAny: false bunu yapıyor: .ts'e çevirdiğin bir dosyada tipini henüz yazmadığın parametreler sessizce any olur ve dosyayı çevirmek bir anda yüzlerce annotation yazmayı gerektirmez.

Dosyaların çoğu çevrildikten sonra noImplicitAny'yi açarsın, ardından gerekiyorsa kapattığın diğer ayarları, örneğin strictNullChecks'i. Her açışta hata sayısı yönetilebilir kalır ve her adım ayrı bir küçük değişiklik olarak birleştirilebilir. Hedef her zaman tek: tsconfig'de geçici olarak kapattığın hiçbir ayar kalmaması.

@ts-expect-error mı @ts-ignore mu?

Geçiş sırasında bazı hataları hemen düzeltemezsin. Bir sonraki satırdaki hatayı susturmanın iki yolu var:

function parsePort(value: string): number {
  return Number(value);
}

// @ts-expect-error eski çağrı, config module'ü taşınınca düzelecek
const legacy = parsePort(8080);

// @ts-ignore
const hidden = parsePort(3000);

// @ts-expect-error
const fine = parsePort("8080");

İkisi de bir sonraki satırdaki hatayı susturur. Fark, hata ortadan kalktığında ortaya çıkar. @ts-ignore sonsuza kadar sessiz kalır; altındaki satır düzelse de, başka bir hataya dönüşse de bir şey söylemez. @ts-expect-error ise hata beklediğini söyler ve hata yoksa kendisi hata verir. Son satırda bunu görüyorsun: parsePort("8080") zaten doğru olduğu için tsc TS2578, "Unused '@ts-expect-error' directive." raporluyor. Yani biri sorunu düzelttiğinde, artık gereksiz olan susturucuyu silmesi gerektiğini derleyici kendisi hatırlatıyor. Geçişte @ts-expect-error kullan ve yanına bir açıklama yaz; sonra bu satırları aratıp birer birer azaltmak bir iş listesine dönüşür.

Sık Düşülen Tuzaklar

Birincisi, checkJs'i baştan bütün projede açmak. Yüzlerce hata görünce ekip geçişten soğur. Dosya dosya // @ts-check ile ilerle; çoğunluk temizlendiğinde checkJs'i aç ve kalanları // @ts-nocheck ile işaretle.

İkincisi, her şeyi any ile susturmak. noImplicitAny: false geçici bir köprü, kalıcı bir ev değil. any'ler biriktikçe TypeScript'in sana sağladığı güvenlik sessizce azalır.

Üçüncüsü, .ts'e çevirdiğin dosyada JSDoc tiplerinin hâlâ çalıştığını sanmak. Çevirdiğin anda tipleri koda taşı.

Dördüncüsü, TypeScript 6 ile temiz denetlenen JSDoc'lu bir projenin 7'de de temiz kalacağını varsaymak. @enum ve Closure tarzı yazımları önceden temizle.

Küçük Bir Deneme

★☆☆ Derlenir mi, tahmin et: baştaki cart.js'te input.value yerine Number(input?.value) yaz. Kaç hata kalır?

★★☆ @enum örneğini TypeScript 7'de çalışacak hâle getir: object'in adını StatusValues yap ve @typedef ile Status adında bir union tanımla. show("deleted") çağrısının artık yakalandığını doğrula.

★★★ Eski bir JavaScript projen varsa, ya da küçük bir tane yazarsan, bu yazıdaki tsconfig'le başla. Önce bir dosyaya // @ts-check ekle, hataları düzelt, sonra dosyayı .ts'e çevir. Her adımda npx tsc --noEmit'in çıktısındaki hata sayısını not et.

Kademeli geçişte en çok zamanı tip denetiminin kendisi alır; büyük projelerde tsc'nin hızı bu yüzden önemli. Bir sonraki yazıda tam da bunun için baştan yazılan derleyiciye, TypeScript 7'ye ve sonrasına bakacağız.