← Kursa Dön
📄 Text · 30 min

İleri Tip Sistemi

TypeScript'in Gizli Süper Gücü

Bir önceki derste Generic'leri öğrendin — tip seviyesinde soyutlama. Bu derste TypeScript'in tip sistemini bir programlama dili gibi kullanmayı öğreneceksin. Evet, doğru duydun: TypeScript'in tip sistemi Turing-complete — yani teorik olarak her hesaplamayı yapabilir. Tabii ki amacımız tip seviyesinde Fibonacci hesaplamak değil, ama bu güç sayesinde son derece ifade gücü yüksek, güvenli tipler oluşturabiliriz.

Bu ders, TypeScript'i "bilenler" ile "ustalar" arasındaki çizgiyi çizen konuları kapsıyor. Discriminated union'lar ile güvenli state management, template literal type'lar ile string manipülasyonu, infer ile tip çıkarımı, recursive type'lar ile iç içe yapılar, branded type'lar ile mantıksal güvenlik. Hazırsan, derinlere dalıyoruz.


Discriminated Unions: Güvenli Durum Yönetimi

Bir trafik lambası düşün: ya kırmızı, ya sarı, ya yeşil. Aynı anda iki renk olamaz. Her renk durumunda farklı kurallar geçerli. İşte Discriminated Union tam olarak bu: bir değişkenin olası durumlarını modellemek ve her durumda farklı davranışı garanti etmek.

Neden Normal Union Yetmez?

// ❌ Normal union — tehlikeli
interface LoadingState {
  loading: boolean;
  data?: string[];
  error?: string;
}

function handleState(state: LoadingState) {
  if (state.loading) {
    // data ve error aynı anda olabilir mi? Bilemeyiz 😱
    console.log(state.data); // undefined olabilir ama TypeScript uyarmaz
  }
}

Problem: loading: true iken data ve error olmamalı ama TypeScript bunu bilmiyor. Her durum için hangi alanların var olduğu belirsiz.

Discriminated Union Çözümü

// ✅ Her durum ayrı bir tip — ayırt edici (discriminant) property ile
type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string; retryCount: number };

function handleRequest(state: RequestState<string[]>) {
  switch (state.status) {
    case "idle":
      console.log("Henüz istek yapılmadı");
      break;

    case "loading":
      console.log("Yükleniyor...");
      break;

    case "success":
      // ✅ TypeScript BİLİYOR ki burada data var
      console.log(`${state.data.length} öğe yüklendi`);
      break;

    case "error":
      // ✅ TypeScript BİLİYOR ki burada error ve retryCount var
      console.log(`Hata: ${state.error}, Deneme: ${state.retryCount}`);
      break;
  }
}

Discriminant (ayırt edici) property burada status. Her union üyesinde farklı bir literal değere sahip. TypeScript switch veya if ile kontrol ettiğinde narrowing yaparak ilgili durumun tipini daraltır.

Exhaustiveness Check

Discriminated Union'ların en güçlü özelliği: yeni bir durum eklendiğinde derleyicinin seni uyarması.

// Gelecekte yeni bir durum eklenirse...
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number }; // ← yeni eklendi!

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;

    case "rectangle":
      return shape.width * shape.height;

    // "triangle" case'ini unuttuk! Derleyici nasıl uyaracak?

    default:
      // never tipine atama: eğer tüm case'ler ele alınmadıysa hata verir
      const _exhaustive: never = shape;
      // ❌ Type 'triangle' is not assignable to type 'never'
      return _exhaustive;
  }
}

never tipine atama hilesi sayesinde, triangle case'ini yazmayı unuttuğunda derleme zamanında hata alırsın. Bu, büyük projelerde hayat kurtarır.

💡 İpucu: Discriminated Union'lar Redux store'larında, API yanıtlarında, form state'lerinde, ödeme akışlarında — kısacası "bir şey birden fazla durumda olabilir" dediğin her yerde kullan.


Template Literal Types: String Seviyesinde Tip Güvenliği

JavaScript dünyasında string'ler her yerde: CSS property isimleri, event adları, API endpoint'leri, veritabanı sorguları... TypeScript 4.1 ile bu string'leri tip seviyesinde manipüle edebilirsin.

Template literal type'lar, JavaScript'teki template literal'lerin (${...}) tip versiyonu:

// Basit birleştirme
type Greeting = `Merhaba ${string}`;

const g1: Greeting = "Merhaba Ali";      // ✅
const g2: Greeting = "Merhaba Dünya";    // ✅
const g3: Greeting = "Günaydın Ali";     // ❌ "Merhaba" ile başlamıyor

// Union ile kombinasyon — çarpım (cartesian product) oluşturur
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";
type CSSClass = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" | "blue-sm" | "blue-md" | "blue-lg" | "green-sm" | "green-md" | "green-lg"
// 3 × 3 = 9 olası değer, hepsi otomatik!

String Manipulation Types

TypeScript, string tiplerini dönüştürmek için yerleşik utility type'lar sunar:

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;        // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

Gerçek Dünya: Event Handler Tipleri

// Bir nesnenin her property'si için "onChange" handler'ı oluştur
type PropEventSource<T> = {
  on<K extends string & keyof T>(
    eventName: `${K}Changed`,
    callback: (newValue: T[K]) => void
  ): void;
};

interface UserConfig {
  name: string;
  age: number;
  darkMode: boolean;
}

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

const user = makeWatchedObject({
  name: "Ali",
  age: 25,
  darkMode: false,
});

// ✅ "nameChanged" geçerli, callback string alır
user.on("nameChanged", (newValue) => {
  // newValue: string — otomatik çıkarım!
  console.log(`İsim değişti: ${newValue}`);
});

// ✅ "ageChanged" geçerli, callback number alır
user.on("ageChanged", (newValue) => {
  // newValue: number
  console.log(`Yaş değişti: ${newValue}`);
});

// ❌ "emailChanged" yok, UserConfig'te email yok
user.on("emailChanged", () => {}); // Hata!

Bu pattern Vue.js, Svelte gibi framework'lerin reaktivite sistemlerinin temelini oluşturur.


infer Keyword: Tip Dedektifi

infer, conditional type içinde kullanılan bir anahtar kelime — bir tipin iç yapısını analiz edip parçalarını çıkarmanı sağlar. Tıpkı bir dedektifin ipuçlarından sonuç çıkarması gibi, infer de tip yapısından bilgi çıkarır.

Temel Kullanım

// Bir fonksiyonun dönüş tipini çıkar
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = MyReturnType<() => string>;           // string
type B = MyReturnType<(x: number) => boolean>; // boolean
type C = MyReturnType<string>;                 // never (fonksiyon değil)

infer R diyor ki: "Bu tip bir fonksiyonsa, dönüş tipini R olarak yakala ve döndür." R bir tip değişkeni gibi çalışır, ama sadece conditional type'ın true dalında kullanılabilir.

Promise İçini Açma

// Promise'in içindeki tipi çıkar
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<string>>;     // string
type E = UnwrapPromise<Promise<number[]>>;   // number[]
type F = UnwrapPromise<string>;              // string (Promise değil)

// İç içe Promise'leri de çöz — recursive!
type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>  // Eğer U da Promise ise, tekrar çöz
  : T;

type G = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>; // string

Tuple'dan Tip Çıkarma

// Tuple'ın ilk elemanının tipini çıkar
type First<T extends any[]> = T extends [infer Head, ...any[]] ? Head : never;

type H = First<[string, number, boolean]>;  // string
type I = First<[42, "hello"]>;               // 42
type J = First<[]>;                          // never

// Son elemanı çıkar
type Last<T extends any[]> = T extends [...any[], infer Tail] ? Tail : never;

type K = Last<[string, number, boolean]>;   // boolean

Birden Fazla infer

// Fonksiyonun hem parametrelerini hem dönüş tipini çıkar
type FunctionParts<T> = T extends (...args: infer P) => infer R
  ? { params: P; returnType: R }
  : never;

type Parts = FunctionParts<(name: string, age: number) => boolean>;
// { params: [string, number]; returnType: boolean }

⚠️ Dikkat: infer sadece extends koşulunun sağ tarafında kullanılabilir. Ve yakalanan tip değişkeni sadece true dalında (? sonrası) geçerlidir. false dalında (: sonrası) kullanamazsın.


Recursive Types: Kendini Tekrarlayan Yapılar

Doğada fraktallar gibi düşün — bir yapı kendini tekrar eder: ağacın dalı da ağaç gibidir, klasörün içindeki klasör de klasördür. Recursive type'lar, iç içe geçmiş yapıları modellemek için kullanılır.

JSON Tipi

JSON'un kendisi recursive bir yapıdır: bir JSON değeri, içinde başka JSON değerleri barındıran bir nesne olabilir:

// JSON'u TypeScript'te modellemek
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]           // ← kendini referans ediyor!
  | { [key: string]: JsonValue }; // ← burada da!

// Geçerli JSON değerleri
const valid1: JsonValue = "hello";
const valid2: JsonValue = { name: "Ali", scores: [1, 2, 3] };
const valid3: JsonValue = {
  nested: {
    deeply: {
      value: [1, "two", { three: true }]
    }
  }
};

// Geçersiz: fonksiyon JSON değeri olamaz
const invalid: JsonValue = () => {}; // ❌

Deep Partial

Daha önce Partial<T>'yi gördün — sadece birinci seviye property'leri opsiyonel yapar. Ya iç içe nesnelerde de opsiyonel yapmak istersen?

// Basit Partial sadece ilk seviyeyi etkiler
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  logging: {
    level: string;
    file: string;
  };
}

type ShallowPartial = Partial<Config>;
// database hâlâ { host: string; port: number; ... } — iç yapı zorunlu!

// ✅ Recursive Deep Partial
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

type DeepPartialConfig = DeepPartial<Config>;
// Artık her seviyede her alan opsiyonel:
const partialConfig: DeepPartialConfig = {
  database: {
    host: "localhost",
    // port, credentials yok — sorun değil!
  },
  // logging hiç yok — sorun değil!
};

Recursive Tip ile Dosya Sistemi

// Dosya sistemi ağacı — klasörler iç içe olabilir
interface FileNode {
  name: string;
  type: "file";
  size: number;
}

interface FolderNode {
  name: string;
  type: "folder";
  children: FileSystemNode[]; // ← recursive!
}

type FileSystemNode = FileNode | FolderNode;

const project: FileSystemNode = {
  name: "src",
  type: "folder",
  children: [
    { name: "index.ts", type: "file", size: 1024 },
    {
      name: "components",
      type: "folder",
      children: [
        { name: "Button.tsx", type: "file", size: 512 },
        { name: "Modal.tsx", type: "file", size: 768 },
      ],
    },
  ],
};

// Recursive fonksiyon — dosya sistemi ağacını düzleştir
function getAllFiles(node: FileSystemNode): FileNode[] {
  if (node.type === "file") return [node];
  return node.children.flatMap(child => getAllFiles(child));
}

console.log(getAllFiles(project));
// [{ name: "index.ts", ... }, { name: "Button.tsx", ... }, { name: "Modal.tsx", ... }]

Tip Seviyesinde Recursive İşlemler

// Nesne yollarını (paths) otomatik çıkar — "database.credentials.username" gibi
type NestedKeyOf<T extends object> = {
  [K in keyof T & string]: T[K] extends object
    ? K | `${K}.${NestedKeyOf<T[K]>}`
    : K;
}[keyof T & string];

type ConfigPaths = NestedKeyOf<Config>;
// "database" | "database.host" | "database.port" |
// "database.credentials" | "database.credentials.username" |
// "database.credentials.password" | "logging" | "logging.level" | "logging.file"

function getConfigValue(config: Config, path: NestedKeyOf<Config>): unknown {
  return path.split(".").reduce((obj: any, key) => obj[key], config);
}

getConfigValue(myConfig, "database.host");          // ✅
getConfigValue(myConfig, "database.credentials.username"); // ✅
getConfigValue(myConfig, "database.invalid");       // ❌ Derleme hatası!

⚠️ Dikkat: TypeScript recursive tiplerde derinlik limiti var. Çok derin yapılarda Type instantiation is excessively deep and possibly infinite hatası alabilirsin. Genelde 20-50 seviye yeterli, ama ihtiyaca göre tasarla.


Branded Types: Mantıksal Tip Güvenliği

TypeScript'in tip sistemi yapısal (structural) — yani iki tip aynı şekle sahipse birbirinin yerine kullanılabilir. Bu genelde istenen bir davranış, ama bazen problem yaratır.

Düşün: userId ve orderId ikisi de number. TypeScript bunları ayırt edemez:

// ❌ Problem: yapısal olarak aynı, mantıksal olarak farklı
type UserId = number;
type OrderId = number;

function getUser(id: UserId): void { /* ... */ }
function getOrder(id: OrderId): void { /* ... */ }

const userId: UserId = 42;
const orderId: OrderId = 99;

getUser(orderId);  // ✅ TypeScript izin verir — ama bu BUG! 🐛
getOrder(userId);  // ✅ TypeScript izin verir — bu da BUG! 🐛

İki farklı kavram (kullanıcı ID'si, sipariş ID'si) TypeScript gözünde aynı şey. Branded Type'lar bu sorunu çözer.

Brand Oluşturma

// Unique brand ile tipleri ayırt et
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<number, "UserId">;
type OrderId = Brand<number, "OrderId">;
type Email = Brand<string, "Email">;
type URL = Brand<string, "URL">;

// Direkt atama yapılamaz — "branding" fonksiyonu gerekir
function createUserId(id: number): UserId {
  // Doğrulama yapılabilir
  if (id <= 0) throw new Error("ID pozitif olmalı");
  return id as UserId;
}

function createOrderId(id: number): OrderId {
  if (id <= 0) throw new Error("ID pozitif olmalı");
  return id as OrderId;
}

function createEmail(value: string): Email {
  if (!value.includes("@")) throw new Error("Geçersiz email");
  return value as Email;
}

// Artık karıştırma imkansız
function getUser(id: UserId): void { /* ... */ }
function getOrder(id: OrderId): void { /* ... */ }

const userId = createUserId(42);
const orderId = createOrderId(99);

getUser(userId);    // ✅
getUser(orderId);   // ❌ Type 'OrderId' is not assignable to type 'UserId'
getOrder(orderId);  // ✅
getOrder(userId);   // ❌ Type 'UserId' is not assignable to type 'OrderId'

Doğrulama (Validation) ile Birleştirme

Branded Type'ların gerçek gücü: bir değerin belirli kurallara uyduğunu tip seviyesinde garanti etmek.

type PositiveNumber = Brand<number, "Positive">;
type NonEmptyString = Brand<string, "NonEmpty">;
type Percentage = Brand<number, "Percentage">;

function toPositive(n: number): PositiveNumber {
  if (n <= 0) throw new Error(`${n} pozitif değil`);
  return n as PositiveNumber;
}

function toNonEmpty(s: string): NonEmptyString {
  if (s.trim().length === 0) throw new Error("Boş string");
  return s as NonEmptyString;
}

function toPercentage(n: number): Percentage {
  if (n < 0 || n > 100) throw new Error(`${n} yüzde aralığında değil`);
  return n as Percentage;
}

// Kullanım: fonksiyon imzası doğrulanmış veri gerektiriyor
function calculateDiscount(
  price: PositiveNumber,
  discount: Percentage
): PositiveNumber {
  const result = price * (1 - discount / 100);
  return toPositive(result);
}

const price = toPositive(100);
const discount = toPercentage(20);
calculateDiscount(price, discount); // ✅

calculateDiscount(100, 20);         // ❌ number ≠ PositiveNumber
// Derleyici: "Önce doğrula, sonra kullan!" diyor

💡 İpucu: Branded Type'lar runtime'da bir maliyet getirmez — __brand property'si sadece tip seviyesinde var, JavaScript çıktısına yansımaz. Sıfır overhead ile tip güvenliği!


Gelişmiş Pattern: Type-Safe Builder

Tüm ileri tipleri birleştiren bir örnek — Builder pattern ile tipler:

// Zorunlu alanları takip eden Builder
type RequiredFields = "host" | "port" | "database";

type BuilderState<Filled extends string = never> = {
  host?: string;
  port?: number;
  database?: string;
  ssl?: boolean;
};

class ConnectionBuilder<Filled extends string = never> {
  private config: BuilderState = {};

  host(value: string): ConnectionBuilder<Filled | "host"> {
    this.config.host = value;
    return this as any;
  }

  port(value: number): ConnectionBuilder<Filled | "port"> {
    this.config.port = value;
    return this as any;
  }

  database(value: string): ConnectionBuilder<Filled | "database"> {
    this.config.database = value;
    return this as any;
  }

  ssl(value: boolean): ConnectionBuilder<Filled> {
    this.config.ssl = value;
    return this as any;
  }

  // build() sadece tüm zorunlu alanlar dolduğunda çağrılabilir!
  build(
    this: ConnectionBuilder<RequiredFields>
  ): Required<BuilderState> {
    return this.config as Required<BuilderState>;
  }
}

// Kullanım
const config = new ConnectionBuilder()
  .host("localhost")
  .port(5432)
  .database("mydb")
  .ssl(true)
  .build(); // ✅ Tüm zorunlu alanlar dolu

const incomplete = new ConnectionBuilder()
  .host("localhost")
  .port(5432)
  .build(); // ❌ database eksik — derleme hatası!

Yaygın Hatalar ve Tuzaklar

1. Union vs Intersection Karıştırma

// Union (|): A VEYA B
type StringOrNumber = string | number;
// Bu tipte string DE olabilir, number DA olabilir

// Intersection (&): A VE B
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// Bu tipte name VE age ikisi de zorunlu

// ⚠️ Primitive intersection — genelde never!
type Impossible = string & number; // never — hiçbir değer hem string hem number olamaz

2. Distributive Conditional Type Tuzağı

type IsNever<T> = T extends never ? true : false;

// Beklenti: true
type Test = IsNever<never>; // Sonuç: never! (true değil!)

// Neden? never boş union gibi davranır, dağıtılacak üye yok
// Çözüm: dağıtmayı engelle
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Test2 = IsNeverFixed<never>; // true ✅

3. Branded Type'da as Kullanımı

// ❌ Brand'i atlamak mümkün — disiplin gerekiyor
const sneakyId = 42 as UserId; // TypeScript izin verir ama kötü pratik!

// ✅ Doğru yol: her zaman factory fonksiyonu kullan
const propertyId = createUserId(42);

Özet

  • Discriminated Union: Ortak bir kind/status/type property'si ile ayırt edilen union. switch ile güvenli narrowing, never ile exhaustiveness check.

  • Template Literal Types: String tipleri birleştirme, dönüştürme (Uppercase, Capitalize), pattern oluşturma. Event isimleri, CSS class'ları, API yolları için ideal.

  • infer: Conditional type içinde tip parçalarını yakalamak için. ReturnType, Parameters gibi utility type'ların arkasındaki mekanizma.

  • Recursive Types: Kendini referans eden tipler — JSON, dosya sistemi, ağaç yapıları. DeepPartial, NestedKeyOf gibi derin dönüşümler.

  • Branded Types: Yapısal olarak aynı ama mantıksal olarak farklı tipleri ayırt etmek. Doğrulama ile birleştirildiğinde "impossible state" kavramını ortadan kaldırır.

  • TypeScript'in tip sistemi bir araç — gereksiz karmaşıklıktan kaçın, ama ihtiyaç olduğunda bu güçten faydalan.