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

TypeScript Declaration Dosyaları: .d.ts, @types, declare ve Global Augmentation

Mert Özen 3 Kas 2026 8 dk 1 görüntülenme
TypeScript Declaration Dosyaları: .d.ts, @types, declare ve Global Augmentation

.d.ts dosyalarının ne olduğunu, @types ve DefinitelyTyped'ı, declare module ile kendi tanımını yazmayı, ambient tanımları, declare global ile global augmentation'ı ve declaration: true ile .d.ts üretmeyi görüyoruz.

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

Önce Tahmin Et

Sayfa yüklenirken sunucu window'a bir konfigürasyon object'i koyuyor ve biz bunu TypeScript'e tanıtmak istiyoruz. Bunun için ayrı bir dosyada Window interface'ine yeni bir property ekliyoruz:

// global.ts
interface AppConfig {
  apiUrl: string;
}

interface Window {
  appConfig: AppConfig;
}

export {};

// main.ts
console.log(window.appConfig.apiUrl);

Temel seride interface'lerin declaration merging ile birleştiğini görmüştük. Sence main.ts derlenir mi? Derlenmezse hata hangi satırda ve neden? İpucu: global.ts'teki tek satırlık export {} masum değil.

.d.ts Nedir?

Declaration dosyası, uzantısı .d.ts olan ve yalnızca tip bilgisi taşıyan bir dosyadır. İçinde fonksiyon gövdesi, değişken değeri, çalışma zamanında çalışacak tek bir satır yoktur; yalnızca "şu isimde şu tipte bir şey var" der. Bir önceki yazıda gördüğümüz, iz bırakan syntax'ların tam tersi: bu dosyaların JavaScript çıktısı hiç yoktur.

Gövde yazmaya kalkarsan TypeScript izin vermez:

// math.d.ts
// Hata: An implementation cannot be declared in ambient contexts.
export function double(n: number): number {
  return n * 2;
}

Farkında olmadan her gün declaration dosyası kullanıyorsun. document.querySelector'ın dönüş tipini, Array.prototype.map'in imzasını TypeScript nereden biliyor? tsconfig'deki lib ayarının seçtiği lib.dom.d.ts ve lib.es2025.d.ts gibi dosyalardan. Editörde bir tarayıcı API'sinde "Go to Definition" dediğinde açılan dosya bunlardan biri.

Bir JavaScript Dosyasına Tip Vermek

En basit senaryo: projende eskiden kalma, düz JavaScript ile yazılmış bir dosya var ve onu TypeScript'ten kullanmak istiyorsun.

// format.js
export function formatPrice(amount, currency) {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}

export const DEFAULT_CURRENCY = "USD";

Yanına aynı isimde bir .d.ts koyarsın:

// format.d.ts
export declare function formatPrice(amount: number, currency: string): string;

export declare const DEFAULT_CURRENCY: string;

declare anahtar kelimesi "bu şey başka bir yerde tanımlı, ben yalnızca tipini söylüyorum" demek. Artık import { formatPrice } from "./format.js" yazdığında TypeScript JavaScript dosyasını okumaz, yanındaki format.d.ts'i bulur ve oradaki tipleri kullanır. formatPrice("12", "USD") yazarsan bildiğin TS2345 hatasını alırsın.

Bir şeyi unutma: TypeScript .d.ts'teki tiplerin JavaScript koduyla gerçekten uyuştuğunu denetlemez. .d.ts bir söz; o sözü tutmak dosyayı yazanın sorumluluğu.

@types ve DefinitelyTyped

npm'deki library'lerin bir kısmı kendi tiplerini paketin içinde getirir; kurduğun anda tipler de gelir. Bir kısmı ise düz JavaScript'tir ve tip içermez. Böyle bir paketi import ettiğinde TypeScript şöyle uyarır (legacy-colors burada uydurma bir paket adı):

error TS7016: Could not find a declaration file for module 'legacy-colors'.

Mesaj devamında paketin JavaScript dosyasının "implicitly has an 'any' type" olduğunu söyler ve iki çözüm önerir: npm i --save-dev @types/legacy-colors ya da kendi .d.ts dosyan. İlki DefinitelyTyped'a gider: tipini kendisi getirmeyen binlerce library için topluluğun yazdığı declaration dosyalarının toplandığı büyük bir GitHub deposu. Oradaki her paket npm'e @types/ ön ekiyle yayımlanır. Kurduğunda TypeScript import'u node_modules/@types/legacy-colors altındaki .d.ts'e çözer; senin ek bir ayar yapmana gerek yoktur.

Global tanımlar ise farklı. @types/node gibi paketler process gibi her yerde var olan global'ler tanımlar. TS 6.0'dan beri tsconfig'deki types ayarının varsayılanı boş bir array, yani @types altındaki paketlerin global'leri artık kendiliğinden yüklenmez:

// Hata: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
console.log(process.env.HOME);

Mesaj ne yapacağını zaten söylüyor: paketi kur ve "types": ["node"] ile tsconfig'e ekle. Hangi global tiplerin projeye girdiğini böylece açıkça sen seçersin.

declare module: Kendi Tanımını Yazmak

DefinitelyTyped'da paketin tipi yoksa ya da yalnızca birkaç fonksiyonunu kullanıyorsan, tanımı kendin yazarsın. Projede örneğin types adında bir klasör aç ve tsconfig'in include listesine ekle:

// types/legacy-colors.d.ts
declare module "legacy-colors" {
  export const colorNames: readonly string[];
  export function isColorName(value: string): boolean;
}

declare module "legacy-colors", tırnak içindeki isimle import edilen module'ün içinde ne olduğunu anlatır. Artık import { isColorName } from "legacy-colors" tip denetiminden geçer ve isColorName(42) hata verir. Acelen varsa gövdesiz kısa yazım da var: declare module "legacy-colors";. Bu, TS7016'yı susturur ama module'den gelen her şeyi any yapar; geçici bir çözüm olarak düşün.

Aynı syntax joker karakter de kabul eder. Bundler kullanan projelerde CSS gibi JavaScript olmayan dosyaları import etmek yaygındır ve TypeScript bunları kendi başına tanımaz:

// types/assets.d.ts
declare module "*.css" {
  const classNames: Record<string, string>;
  export default classNames;
}

Bu tanım olmadan import "./styles.css" satırı TS 6.0'dan beri varsayılan olarak açık gelen noUncheckedSideEffectImports yüzünden "Cannot find module or type declarations for side-effect import of './styles.css'." (TS2882) hatası verir. Tanımla birlikte import styles from "./button.css" yazıp styles.primary'yi string olarak kullanabilirsin.

Ambient Tanımlar

Bazı değerler hiçbir module'den gelmez, ortamda hazır bulunur. Build aracının derleme sırasında koda gömdüğü bir sürüm numarası buna iyi bir örnek. Bunlara ambient tanım denir ve declare ile yazılır:

// types/build.d.ts
declare const __APP_VERSION__: string;

Bu dosyada hiç import ya da export yok. Module'ler yazısındaki ayrımı hatırla: böyle bir dosya script'tir ve içindeki tanımlar global scope'a düşer. Bu yüzden projedeki her dosya __APP_VERSION__'ı import etmeden kullanabilir. Aynı dosyaya bir export eklersen tanım o dosyaya hapsolur ve başka hiçbir yerden görünmez.

Bir library'yi tek bir global object üzerinden sunan eski script'ler için de aynı yol geçerli. Önceki yazıda söz verdiğimiz yer burası: declare namespace ile tipleri bir isim altında toplayabilirsin ve bu, çalışma zamanına hiçbir şey bırakmadığı için tamamen erasable'dır.

Global Augmentation

Baştaki sorunun cevabı da bu ayrımda. global.ts'in sonundaki export {} dosyayı bir module yapıyor. Bir module'ün içinde yazdığın interface Window, global Window'la birleşmez; o module'e ait, yeni ve kimsenin kullanmadığı bir interface olur. main.ts ise global Window'a bakar:

main.ts(1,20): error TS2339: Property 'appConfig' does not exist on type 'Window & typeof globalThis'.

Bir module'ün içinden global scope'a ekleme yapmanın yolu declare global:

// global.ts
interface AppConfig {
  apiUrl: string;
  debug: boolean;
}

declare global {
  interface Window {
    appConfig: AppConfig;
  }
}

export {};

Buna global augmentation denir. declare global bloğu yalnızca bir module'ün içinde yazılabilir; export {} satırının orada durma sebebi bu. Artık window.appConfig.apiUrl her dosyada string olarak görünür. AppConfig'in kendisi ise module'de kaldı; global scope'u gereksiz isimlerle doldurmadık.

declaration: true ile .d.ts Üretmek

Şimdiye kadar .d.ts'leri elle yazdık. Kodun zaten TypeScript ise bunu tsc senin yerine yapar. Bir library yayımlarken kullanıcılarına JavaScript ile birlikte tipleri de verirsin:

// src/cart.ts
export interface CartItem {
  sku: string;
  price: number;
  quantity: number;
}

const TAX_RATE = 0.2;

export function cartTotal(items: readonly CartItem[]) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  return Math.round(subtotal * (1 + TAX_RATE) * 100) / 100;
}

export class Cart {
  #items: CartItem[] = [];

  add(item: CartItem): this {
    this.#items.push(item);
    return this;
  }

  get total() {
    return cartTotal(this.#items);
  }
}
{
  "compilerOptions": {
    "module": "nodenext",
    "target": "esnext",
    "rootDir": "src",
    "outDir": "dist",
    "declaration": true
  },
  "include": ["src"]
}

npx tsc sonrasında dist klasöründe cart.js'in yanında şu cart.d.ts durur:

export interface CartItem {
  sku: string;
  price: number;
  quantity: number;
}
export declare function cartTotal(items: readonly CartItem[]): number;
export declare class Cart {
  #private;
  add(item: CartItem): this;
  get total(): number;
}

Birkaç ayrıntı dikkat çekiyor. cartTotal'ın dönüş tipini yazmamıştık; tsc inference ile number buldu ve dosyaya yazdı. Export edilmeyen TAX_RATE hiç yok, çünkü dışarıdan görünmüyor. #items ise yalnızca #private; olarak geçiyor; class'ın private bir alanı olduğu bilgisi kalıyor ama tipi ve adı sızmıyor. Yalnızca .d.ts istiyorsan, örneğin JavaScript'i başka bir araç üretiyorsa, emitDeclarationOnly ekleyebilirsin.

tsconfig'deki rootDir satırı süs değil. TS 6.0'dan beri rootDir'in varsayılanı tsconfig'in bulunduğu klasör. Kaynaklar src altındayken bu satır olmazsa tsc 7.0 TS5011 hatası verir ve rootDir'i açıkça yazmanı ister.

Paketi yayımlarken tiplerin yerini package.json'da gösterirsin. exports alanında types koşulu ilk sırada olmalı:

{
  "name": "cart-utils",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/cart.d.ts",
      "default": "./dist/cart.js"
    }
  }
}

Sık Düşülen Tuzaklar

Global bir ekleme yaparken dosyanın module olduğunu fark etmemek. Tek bir import satırı bile dosyayı module yapar ve declare global olmadan yaptığın ekleme sessizce yerel kalır.

Kısa declare module "x"; yazımını kalıcı hâle getirmek. Hata kaybolur ama o library'den gelen her şey any olur; tip denetimi o noktada fiilen kapanır.

.d.ts'in doğruluğuna körü körüne güvenmek. Elle yazılmış ya da @types'tan gelen tipler, library'nin kurulu sürümünden geri kalmış olabilir. Paketi güncellerken ilgili @types paketini de güncelle.

.d.ts'i elle yazıp TypeScript kodunun yanında tutmak. Kaynak zaten TypeScript ise declaration: true kullan; elle yazılan kopya bir gün mutlaka koddan kopar.

Küçük Bir Deneme

★☆☆ Yazıdaki format.js ve format.d.ts'i bir klasöre koy, onları import eden bir main.ts yaz. formatPrice("12", "USD") çağrısının hata verdiğini, formatPrice(12, "USD")'nin geçtiğini gör.

★★☆ Tahmin et, sonra dene: types/build.d.ts dosyasının sonuna export {}; eklersen console.log(__APP_VERSION__) satırı derlenir mi? Neden?

★★☆ Önce Tahmin Et bölümündeki global.ts'i declare global kullanmadan düzelt: dosyayı module olmaktan çıkar. Bu yolun dezavantajı ne olabilir?

★★★ cart.ts'e export type Currency = "USD" | "EUR"; ve bu type'ı parametre olarak alan bir fonksiyon ekle. declaration: true ile derlemeden önce .d.ts'te nasıl görüneceğini kâğıda yaz, sonra çıktıyla karşılaştır.

Bu yazıyla Orta seri tamamlandı: generic'lerden utility type'lara, type guard'lardan module'lere ve declaration dosyalarına kadar gündelik TypeScript'in neredeyse tamamını gördük. İleri seride tipleri yazmaktan tipleri üretmeye geçiyoruz. Conditional type'larla tipler arasında karar verecek, infer ile bir tipin içinden parça yakalayacak, mapped type'larla ve template literal type'larla mevcut tiplerden yenilerini türeteceğiz; Orta seride kullandığımız ReturnType gibi utility type'ları kendimiz yazacak ve tip seviyesinde programlamanın sınırlarını zorlayacağız. Yolun sonunda decorator'lara ve TypeScript 7'nin Go ile yazılmış derleyicisinin ayrıntılarına da ineceğiz.