İçeriğe geç
·18 dk okuma

TypeScript İleri Seviye Tipler: Conditional, Mapped ve Template Literal Büyüsü

TypeScript'in en güçlü tip özelliklerine derinlemesine bakış — conditional type'lar, mapped type'lar, infer anahtar kelimesi ve template literal type'lar. Gerçekten kullanacağın pratik kalıplar.

Paylaş:X / TwitterLinkedIn

Çoğu TypeScript geliştiricisi aynı noktada takılıp kalır. Interface'leri, generic'leri, union type'ları bilirler. Bir React bileşenini tipleyebilirler. Kırmızı dalgalı çizgileri ortadan kaldırabilirler. Ama tip sistemini zorunlu bir kötülük olarak görürler — hata önlemek için var olan, aktif olarak daha iyi yazılım tasarlamalarına yardımcı olmayan bir şey.

Ben o düzlükte yaklaşık iki yıl kaldım. Beni oradan çıkaran şey, TypeScript'in tip sisteminin kendi başına bir programlama dili olduğunu fark etmemdi. Koşulları, döngüleri, desen eşleme, özyineleme ve string manipülasyonu var. Bunu bir kez içselleştirdiğinde her şey değişiyor. Derleyiciyle savaşmayı bırakıp onunla iş birliği yapmaya başlıyorsun.

Bu yazı, üretim kodunda sürekli kullandığım tip-seviyesi özellikleri kapsıyor. Akademik alıştırmalar değil — beni gerçek hatalardan kurtaran gerçek kalıplar.

Tip-Seviyesi Programlama Zihniyeti#

Sözdizime dalmadan önce, tipler hakkındaki düşünce biçimini yeniden çerçevelemek istiyorum.

Değer-seviyesi programlamada, çalışma zamanında veriyi dönüştüren fonksiyonlar yazarsın:

typescript
function extractIds(users: User[]): number[] {
  return users.map(u => u.id);
}

Tip-seviyesi programlamada ise derleme zamanında diğer tipleri dönüştüren tipler yazarsın:

typescript
type ExtractIds<T extends { id: unknown }[]> = T[number]["id"];

Zihinsel model aynı — girdi, dönüşüm, çıktı. Fark şu ki tip-seviyesi kod derleme sırasında çalışır, çalışma zamanında değil. Sıfır JavaScript üretir. Tek amacı imkansız durumları temsil edilemez kılmaktır.

Eğer daha önce dört parametreden üçünün sadece belirli kombinasyonlarda anlamlı olduğu veya dönüş tipinin girdiye bağlı olduğu bir fonksiyon yazdıysan, zaten tip-seviyesi programlamaya ihtiyaç duymuşsundur. Sadece TypeScript'in bunu ifade edebildiğini bilmiyor olabilirsin.

Conditional Type'lar#

Conditional type'lar, tip sisteminin if/else'idir. Sözdizimi ternary operatörüne benzer:

typescript
type IsString<T> = T extends string ? true : false;
 
type A = IsString<"hello">; // true
type B = IsString<42>;      // false
type C = IsString<string>;  // true

Buradaki extends anahtar kelimesi kalıtım anlamına gelmiyor. "Atanabilir mi?" anlamına geliyor. Bunu "T, string şekline sığar mı?" olarak düşün.

Kendi Yardımcı Tiplerini Oluşturmak#

TypeScript'in yerleşik yardımcı tiplerinden bazılarını yeniden inşa ederek nasıl çalıştıklarını anlayalım.

NonNullable — bir union'dan null ve undefined'ı kaldırır:

typescript
type MyNonNullable<T> = T extends null | undefined ? never : T;
 
type Test = MyNonNullable<string | null | undefined>;
// Sonuç: string

never tipi "boş küme"dir — bir union'dan bir şeyi kaldırır. Bir conditional type dalı never'a çözümlendiğinde, union'un o üyesi basitçe kaybolur.

Extract — yalnızca bir kısıtlamayla eşleşen union üyelerini tutar:

typescript
type MyExtract<T, U> = T extends U ? T : never;
 
type Numbers = Extract<string | number | boolean, number | boolean>;
// Sonuç: number | boolean

Exclude — tam tersi, eşleşen üyeleri kaldırır:

typescript
type MyExclude<T, U> = T extends U ? never : T;
 
type WithoutStrings = Exclude<string | number | boolean, string>;
// Sonuç: number | boolean

Dağıtıcı (Distributive) Conditional Type'lar#

İşte işlerin ilginçleştiği ve çoğu insanın kafasının karıştığı yer. Bir conditional type'a union type geçirdiğinde, TypeScript koşulu union'un her üyesine ayrı ayrı dağıtır.

typescript
type ToArray<T> = T extends unknown ? T[] : never;
 
type Result = ToArray<string | number>;
// Beklediğin: (string | number)[]
// Gerçek sonuç: string[] | number[]

TypeScript (string | number) extends unknown'ı değerlendirmez. Bunun yerine string extends unknown ve number extends unknown'ı ayrı ayrı değerlendirir, sonra sonuçları birleştirir.

Extract ve Exclude'un bu şekilde çalışmasının nedeni budur. Dağıtım, T "çıplak" bir tip parametresi olduğunda (hiçbir şeye sarılmadığında) otomatik olarak gerçekleşir.

Dağıtımı engellemek istiyorsan, her iki tarafı da bir tuple'a sar:

typescript
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
 
type Result = ToArrayNonDist<string | number>;
// Sonuç: (string | number)[]

Bundan itiraf etmek istediğimden daha fazla darbe yedim. Conditional type'ın union girdileriyle beklenmedik sonuçlar veriyorsa, neredeyse her zaman nedeni dağıtımdır.

Pratik Örnek: API Yanıt İşleyici#

İşte başarılı veya başarısız olabilen API yanıtlarıyla uğraşırken kullandığım bir kalıp:

typescript
type ApiResponse<T> =
  | { status: "success"; data: T; error: never }
  | { status: "error"; data: never; error: string };
 
type ExtractData<R> = R extends { status: "success"; data: infer D } ? D : never;
type ExtractError<R> = R extends { status: "error"; error: infer E } ? E : never;
 
type UserResponse = ApiResponse<{ id: number; name: string }>;
 
type UserData = ExtractData<UserResponse>;
// Sonuç: { id: number; name: string }
 
type UserError = ExtractError<UserResponse>;
// Sonuç: string

Bu, her yanıt şekli için manuel tip korumaları yazmaktan çok daha temiz.

infer Anahtar Kelimesi#

infer, tipler için desen eşlemedir. Bir conditional type içinde TypeScript'in senin için çözeceği bir tip değişkeni tanımlamanı sağlar.

Bunu şöyle düşün: "Bu tipin belirli bir şekli olduğunu biliyorum. Umursadığım kısmı çıkar."

Dönüş Tiplerini Çıkarmak#

typescript
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
 
type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // boolean

TypeScript (...args) => bir şey kalıbını görür, fonksiyonunu buna karşı eşler ve dönüş tipini R'ye atar.

Parametre Tiplerini Çıkarmak#

typescript
type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never;
 
type Fn = (x: number, y: string) => void;
type Params = MyParameters<Fn>; // [x: number, y: string]

Promise'leri Açmak#

Bunu async fonksiyonlarla çalışırken sürekli kullanıyorum:

typescript
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
 
type A = UnpackPromise<Promise<string>>;  // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>;            // string (doğrudan geçiş)

Derin iç içe geçmiş promise'ler için:

typescript
type DeepUnpackPromise<T> = T extends Promise<infer U>
  ? DeepUnpackPromise<U>
  : T;
 
type Deep = DeepUnpackPromise<Promise<Promise<Promise<number>>>>;
// Sonuç: number

Dizi Eleman Tiplerini Çıkarmak#

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
 
type Items = ElementOf<string[]>;         // string
type Mixed = ElementOf<(string | number)[]>; // string | number

Tek Tipte Birden Fazla infer#

Farklı parçaları çıkarmak için infer'i birden fazla kez kullanabilirsin:

typescript
type FirstAndLast<T extends unknown[]> =
  T extends [infer First, ...unknown[], infer Last]
    ? { first: First; last: Last }
    : never;
 
type Result = FirstAndLast<[1, 2, 3, 4]>;
// Sonuç: { first: 1; last: 4 }

Gerçek Dünya: Tip-Güvenli Olay Çıkarımı#

İşte olay güdümlü sistemlerde kullandığım bir kalıp:

typescript
type EventMap = {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "order:created": { orderId: string; total: number };
  "order:shipped": { orderId: string; trackingId: string };
};
 
type EventName = keyof EventMap;
type EventPayload<E extends EventName> = EventMap[E];
 
// Tüm kullanıcı ile ilgili olayları çıkar
type UserEvents = Extract<EventName, `user:${string}`>;
// Sonuç: "user:login" | "user:logout"
 
// Sadece kullanıcı olaylarının payload'ını al
type UserEventPayloads = EventPayload<UserEvents>;
// Sonuç: { userId: string; timestamp: number } | { userId: string }

Mapped Type'lar#

Mapped type'lar, mevcut bir tipin her özelliğini dönüştürerek yeni nesne tipleri oluşturmanı sağlar. Tip sisteminin map()'idir.

Temeller#

typescript
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
type Partial<T> = {
  [K in keyof T]?: T[K];
};

keyof T tüm özellik anahtarlarının bir union'ını verir. [K in ...] her birini yineler. T[K] o özelliğin tipidir.

Sıfırdan Yardımcı Tipler Oluşturmak#

Required — tüm özellikleri zorunlu yap:

typescript
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

-? sözdizimi opsiyonel niteleyiciyi kaldırır. Benzer şekilde, -readonly salt okunur niteleyiciyi kaldırır.

Pick — belirli özellikleri seç:

typescript
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};
 
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}
 
type PublicUser = MyPick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

Omit — belirli özellikleri kaldır:

typescript
type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};
 
type SafeUser = MyOmit<User, "password">;
// { id: number; name: string; email: string }

Record — belirli anahtarlar ve değer tipleriyle bir nesne tipi oluştur:

typescript
type MyRecord<K extends string | number | symbol, V> = {
  [P in K]: V;
};
 
type StatusMap = MyRecord<"active" | "inactive" | "banned", boolean>;
// { active: boolean; inactive: boolean; banned: boolean }

as ile Anahtar Yeniden Eşleme#

TypeScript 4.1, özellik adlarını dönüştürmenize izin veren anahtar yeniden eşlemeyi tanıttı:

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
interface Person {
  name: string;
  age: number;
  email: string;
}
 
type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

Ayrıca never'a eşleyerek özellikleri filtreleyebilirsin:

typescript
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
 
type StringProps = OnlyStrings<Person>;
// { name: string; email: string }

Bu inanılmaz güçlü. Aynı anda özellikleri yineliyorsun, filtreliyorsun ve adlarını dönüştürüyorsun — hepsi tip seviyesinde.

Pratik Örnek: Form Doğrulama Tipleri#

Bu kalıbı birkaç form kütüphanesinde kullandım:

typescript
interface FormFields {
  username: string;
  email: string;
  age: number;
  bio: string;
}
 
type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};
 
type TouchedFields<T> = {
  [K in keyof T]?: boolean;
};
 
type DirtyFields<T> = {
  [K in keyof T]?: boolean;
};
 
type FormState<T> = {
  values: T;
  errors: ValidationErrors<T>;
  touched: TouchedFields<T>;
  dirty: DirtyFields<T>;
  isValid: boolean;
  isSubmitting: boolean;
};
 
// Şimdi form durumun tamamen alan bazında tiplenmiş
type MyFormState = FormState<FormFields>;

FormFields'a bir alan ekle ve ilgili her tip otomatik güncellenir. Bir alanı kaldır ve derleyici her referansı yakalar. Bu, tüm hata kategorilerini önleyen türden bir şey.

Template Literal Type'lar#

Template literal type'lar, stringleri tip seviyesinde manipüle etmeni sağlar. JavaScript template literal'larıyla aynı backtick sözdizimini kullanır, ama tipler için.

Temel String Manipülasyonu#

typescript
type Greeting = `Hello, ${string}`;
 
const a: Greeting = "Hello, world";    // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, world";    // Hata!

TypeScript ayrıca yerleşik string manipülasyon tipleri sağlar:

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

Olay Adı Kalıpları#

Template literal type'ların gerçekten parladığı yer burası. İşte tip-güvenli olay sistemleri için kullandığım bir kalıp:

typescript
type EventNames<T extends string> = `${T}Changed` | `${T}Deleted` | `${T}Created`;
 
type ModelEvents = EventNames<"user" | "order" | "product">;
// "userChanged" | "userDeleted" | "userCreated"
// | "orderChanged" | "orderDeleted" | "orderCreated"
// | "productChanged" | "productDeleted" | "productCreated"

İki tip parametresinden dokuz olay. Ve hepsi tip-güvenli.

CSS-in-TypeScript Kalıpları#

Template literal type'lar, CSS kalıplarını tip seviyesinde zorunlu kılabilir:

typescript
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
 
function setWidth(value: CSSValue): void {
  // implementasyon
}
 
setWidth("100px");   // OK
setWidth("2.5rem");  // OK
setWidth("50%");     // OK
// setWidth("100");  // Hata: geçerli bir CSS değeri değil
// setWidth("big");  // Hata

Template Literal'larla String Ayrıştırma#

Template literal type'ları infer ile birlikte stringleri ayrıştırmak için kullanabilirsin:

typescript
type ParseRoute<T extends string> =
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ParseRoute<`/${Rest}`>
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      : never;
 
type RouteParams = ParseRoute<"/users/:userId/posts/:postId">;
// Sonuç: "userId" | "postId"

Bu, tRPC gibi tip-güvenli yönlendirme kütüphanelerinin temelini oluşturur. /users/:userId/posts/:postId stringi, parametre adlarını çıkarmak için tip seviyesinde ayrıştırılır.

Template Literal'ları Mapped Type'larla Birleştirmek#

İşte gerçekten güçlü hale geldiği yer:

typescript
type EventHandlers<T extends Record<string, unknown>> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};
 
interface ThemeConfig {
  color: string;
  fontSize: number;
  darkMode: boolean;
}
 
type ThemeHandlers = EventHandlers<ThemeConfig>;
// {
//   onColorChange: (newValue: string, oldValue: string) => void;
//   onFontSizeChange: (newValue: number, oldValue: number) => void;
//   onDarkModeChange: (newValue: boolean, oldValue: boolean) => void;
// }

Basit bir config interface'inden, doğru parametre tipleriyle tam tiplenmiş olay işleyicileri elde edersin. Bu, gerçekten geliştirme süresinden tasarruf sağlayan türden tip-seviyesi koddur.

İleri Düzey: Nokta-Notasyonu Yol Tipleri#

İşte daha ileri bir kalıp — iç içe geçmiş bir nesne boyunca tüm olası nokta-notasyonu yollarını oluşturmak:

typescript
type DotPaths<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? `${Prefix}${K}` | DotPaths<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;
 
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  server: {
    port: number;
  };
}
 
type ConfigPaths = DotPaths<Config>;
// "database" | "database.host" | "database.port"
// | "database.credentials" | "database.credentials.username"
// | "database.credentials.password" | "server" | "server.port"

Şimdi her olası yolu otomatik tamamlayan ve her biri için doğru tipi döndüren bir getConfig fonksiyonu yazabilirsin:

typescript
type GetValueByPath<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? GetValueByPath<T[Key], Rest>
      : never
    : P extends keyof T
      ? T[P]
      : never;
 
function getConfig<P extends ConfigPaths>(path: P): GetValueByPath<Config, P> {
  // implementasyon
  const keys = path.split(".");
  let result: unknown = config;
  for (const key of keys) {
    result = (result as Record<string, unknown>)[key];
  }
  return result as GetValueByPath<Config, P>;
}
 
const host = getConfig("database.host");         // tip: string
const port = getConfig("database.port");         // tip: number
const username = getConfig("database.credentials.username"); // tip: string

Tam otomatik tamamlama, tam tip güvenliği, tiplerden kaynaklanan sıfır çalışma zamanı yükü.

Özyinelemeli (Recursive) Tipler#

TypeScript özyinelemeli tip tanımlarını destekler, bu da keyfi derinlikte iç içe geçmiş yapıları işlemeni sağlar.

Deep Partial#

Yerleşik Partial<T> yalnızca üst düzey özellikleri opsiyonel yapar. İç içe geçmiş nesneler için özyinelemeye ihtiyacın var:

typescript
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[K]>
    : T[K];
};
 
interface Settings {
  theme: {
    colors: {
      primary: string;
      secondary: string;
    };
    fonts: {
      heading: string;
      body: string;
    };
  };
  notifications: {
    email: boolean;
    push: boolean;
  };
}
 
type PartialSettings = DeepPartial<Settings>;
// Her iç içe geçmiş özellik artık opsiyonel

Dizi kontrolüne dikkat et: bu olmazsa, diziler nesne olarak ele alınır ve sayısal indeksleri opsiyonel olur, ki bu istediğin şey değil.

Deep Readonly#

Aynı kalıp, farklı niteleyici:

typescript
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : T[K] extends Array<infer U>
        ? ReadonlyArray<DeepReadonly<U>>
        : DeepReadonly<T[K]>
    : T[K];
};

Function kontrolü önemli — bu olmazsa, fonksiyon özellikleri iç yapılarına readonly uygulanır, ki bu mantıklı değil.

JSON Tipi#

İşte klasik bir özyinelemeli tip — herhangi bir geçerli JSON değerini temsil etmek:

typescript
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
 
function parseJson(input: string): JsonValue {
  return JSON.parse(input);
}
 
function isJsonObject(value: JsonValue): value is JsonObject {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

Bu, JSON ayrıştırma için any kullanmaktan çok daha iyi. Tüketiciye "bir şey geri alacaksın, ama kullanmadan önce daraltman gerekiyor" der. Ki tam olarak gerçek budur.

İç İçe Menüler İçin Özyinelemeli Tip#

Pratik bir örnek — navigasyon menü yapısını tiplemek:

typescript
interface MenuItem {
  label: string;
  href: string;
  icon?: string;
  children?: MenuItem[];
}
 
type Menu = MenuItem[];
 
const navigation: Menu = [
  {
    label: "Products",
    href: "/products",
    children: [
      {
        label: "Software",
        href: "/products/software",
        children: [
          { label: "IDE", href: "/products/software/ide" },
          { label: "CLI Tools", href: "/products/software/cli" },
        ],
      },
      { label: "Hardware", href: "/products/hardware" },
    ],
  },
  { label: "About", href: "/about" },
];

Özyinelemeli children özelliği, tam tip güvenliğiyle sonsuz iç içe geçme sağlar.

Gerçek Dünya Kalıpları#

Üretim sistemlerinde gerçekten kullandığım kalıplardan bazılarını paylaşayım.

Builder Kalıbı ve Tipler#

Builder kalıbı, tip sistemi nelerin ayarlandığını takip ettiğinde çok daha kullanışlı hale gelir:

typescript
type RequiredKeys = "host" | "port" | "database";
 
type BuilderState = {
  [K in RequiredKeys]?: true;
};
 
class DatabaseConfigBuilder<State extends BuilderState = {}> {
  private config: Partial<{
    host: string;
    port: number;
    database: string;
    ssl: boolean;
    poolSize: number;
  }> = {};
 
  setHost(host: string): DatabaseConfigBuilder<State & { host: true }> {
    this.config.host = host;
    return this as unknown as DatabaseConfigBuilder<State & { host: true }>;
  }
 
  setPort(port: number): DatabaseConfigBuilder<State & { port: true }> {
    this.config.port = port;
    return this as unknown as DatabaseConfigBuilder<State & { port: true }>;
  }
 
  setDatabase(db: string): DatabaseConfigBuilder<State & { database: true }> {
    this.config.database = db;
    return this as unknown as DatabaseConfigBuilder<State & { database: true }>;
  }
 
  setSsl(ssl: boolean): DatabaseConfigBuilder<State> {
    this.config.ssl = ssl;
    return this as unknown as DatabaseConfigBuilder<State>;
  }
 
  setPoolSize(size: number): DatabaseConfigBuilder<State> {
    this.config.poolSize = size;
    return this as unknown as DatabaseConfigBuilder<State>;
  }
 
  build(
    this: DatabaseConfigBuilder<{ host: true; port: true; database: true }>
  ): Required<Pick<typeof this.config, "host" | "port" | "database">> &
    Partial<Pick<typeof this.config, "ssl" | "poolSize">> {
    return this.config as ReturnType<typeof this.build>;
  }
}
 
// Bu derlenir:
const config = new DatabaseConfigBuilder()
  .setHost("localhost")
  .setPort(5432)
  .setDatabase("myapp")
  .setSsl(true)
  .build();
 
// Bu derleme zamanında başarısız olur — .setDatabase() eksik:
// const bad = new DatabaseConfigBuilder()
//   .setHost("localhost")
//   .setPort(5432)
//   .build(); // Hata: 'database' eksik

build() metodu, yalnızca tüm zorunlu alanlar ayarlandığında çağrılabilir hale gelir. Bu, çalışma zamanında değil, derleme zamanında yakalanır.

Tip-Güvenli Olay Yayıcı#

İşte payload tiplerinin zorunlu kılındığı bir olay yayıcı:

typescript
type EventMap = Record<string, unknown>;
 
class TypedEventEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};
 
  on<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }
 
  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.listeners[event]?.forEach((listener) => listener(payload));
  }
 
  off<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    const handlers = this.listeners[event];
    if (handlers) {
      this.listeners[event] = handlers.filter((h) => h !== listener) as typeof handlers;
    }
  }
}
 
// Kullanım
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "notification:new": { message: string; level: "info" | "warning" | "error" };
}
 
const emitter = new TypedEventEmitter<AppEvents>();
 
// Tam tiplenmiş — IDE olay adlarını ve payload şekillerini otomatik tamamlar
emitter.on("user:login", (payload) => {
  console.log(payload.userId);     // string
  console.log(payload.timestamp);  // number
});
 
// Tip hatası: payload eşleşmiyor
// emitter.emit("user:login", { userId: "123" });
// Hata: 'timestamp' eksik
 
// Tip hatası: olay mevcut değil
// emitter.on("user:signup", () => {});
// Hata: "user:signup" AppEvents'te yok

Bu kalıbı üç farklı üretim projesinde kullandım. Tüm olay ile ilgili hata kategorilerini derleme zamanında yakalar.

Ayrıştırılmış Union'lar ve Kapsamlı Kontrol#

Ayrıştırılmış union'lar muhtemelen TypeScript'teki en kullanışlı tek kalıp. Kapsamlı kontrol ile birleştirildiğinde, her durumu ele aldığını garanti eder:

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      // Bu satır kapsamlı kontrolü sağlar
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

Birisi yeni bir şekil varyantı eklerse (diyelim ki "pentagon"), bu fonksiyon derlenmeyecektir çünkü never'a bir değer atanamaz. Derleyici her durumu ele almayı zorlar.

Bunu bir yardımcı fonksiyonla daha da ileri götürüyorum:

typescript
function assertNever(value: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`);
}
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      assertNever(shape, `Unknown shape kind: ${(shape as Shape).kind}`);
  }
}

Tip-Güvenli Durum Makineleri#

Ayrıştırılmış union'lar, durum makinelerini de güzelce modeller:

typescript
type ConnectionState =
  | { status: "disconnected" }
  | { status: "connecting"; attempt: number }
  | { status: "connected"; socket: WebSocket; connectedAt: Date }
  | { status: "error"; error: Error; lastAttempt: Date };
 
type ConnectionAction =
  | { type: "CONNECT" }
  | { type: "CONNECTED"; socket: WebSocket }
  | { type: "DISCONNECT" }
  | { type: "ERROR"; error: Error }
  | { type: "RETRY" };
 
function connectionReducer(
  state: ConnectionState,
  action: ConnectionAction
): ConnectionState {
  switch (state.status) {
    case "disconnected":
      if (action.type === "CONNECT") {
        return { status: "connecting", attempt: 1 };
      }
      return state;
 
    case "connecting":
      if (action.type === "CONNECTED") {
        return {
          status: "connected",
          socket: action.socket,
          connectedAt: new Date(),
        };
      }
      if (action.type === "ERROR") {
        return {
          status: "error",
          error: action.error,
          lastAttempt: new Date(),
        };
      }
      return state;
 
    case "connected":
      if (action.type === "DISCONNECT") {
        state.socket.close();
        return { status: "disconnected" };
      }
      if (action.type === "ERROR") {
        return {
          status: "error",
          error: action.error,
          lastAttempt: new Date(),
        };
      }
      return state;
 
    case "error":
      if (action.type === "RETRY") {
        return { status: "connecting", attempt: 1 };
      }
      if (action.type === "DISCONNECT") {
        return { status: "disconnected" };
      }
      return state;
 
    default:
      assertNever(state, "Unknown connection state");
  }
}

Her durum, yalnızca o durum için anlamlı olan özelliklere sahip. Bağlantı kesildiğinde socket'e erişemezsin. Bağlıyken error'a erişemezsin. Tip sistemi, durum makinesinin kısıtlamalarını zorlar.

Alan Güvenliği İçin Markalı Tipler#

Vazgeçilmez bulduğum bir kalıp daha — aynı temel tipi paylaşan değerlerin birbirine karışmasını önlemek için markalı tipler kullanmak:

typescript
type Brand<T, B extends string> = T & { readonly __brand: B };
 
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
 
function createUserId(id: string): UserId {
  return id as UserId;
}
 
function createOrderId(id: string): OrderId {
  return id as OrderId;
}
 
function getUser(id: UserId): Promise<User> {
  // ...
}
 
function getOrder(id: OrderId): Promise<Order> {
  // ...
}
 
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
 
getUser(userId);   // OK
// getUser(orderId); // Hata! OrderId, UserId'ye atanamaz

Hem UserId hem de OrderId çalışma zamanında string'dir. Ama derleme zamanında ayrı tiplerdir. Bir kullanıcı ID'si beklenen yere sipariş ID'si geçirmek kelimenin tam anlamıyla imkansız. Bu, kullandığım her projede gerçek hataları yakaladı.

Yaygın Tuzaklar#

İleri seviye tipler güçlüdür, ama tuzaklarla birlikte gelir. İşte zor yoldan öğrendiklerim.

Döngüsel Referans Limitleri#

TypeScript'in tipler için özyineleme derinlik sınırı vardır (şu anda yaklaşık 50 seviye, ancak değişir). Çok derine inersen, korkulan "Type instantiation is excessively deep and possibly infinite" hatasını alırsın.

typescript
// Bu, derin iç içe geçmiş nesneler için özyineleme limitine ulaşacak
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
 
// Düzeltme: Derinlik sayacı ekle
type DeepPartialSafe<T, Depth extends number[] = []> =
  Depth["length"] extends 10
    ? T
    : {
        [K in keyof T]?: T[K] extends object
          ? DeepPartialSafe<T[K], [...Depth, 0]>
          : T[K];
      };

Derinlik sayacı hilesi, her özyinelemeli adımda büyüyen bir tuple kullanır. Limitine ulaştığında, özyineleme durur.

Performans Etkileri#

Karmaşık tipler editörünü ve derleme sürelerini önemli ölçüde yavaşlatabilir. Tek bir aşırı zekice tipin her tuş vuruşunun geri bildirim döngüsüne 3 saniye eklediği projeler gördüm.

Uyarı işaretleri:

  • IDE'nin otomatik tamamlamayı göstermesi 2 saniyeden fazla sürüyor
  • tsc --noEmit bir tip ekledikten sonra fark edilir şekilde daha uzun sürüyor
  • "Type instantiation is excessively deep" hataları görüyorsun
typescript
// Bu çok akıllıca — kombinatoryal patlama oluşturuyor
type AllCombinations<T extends string[]> = T extends [
  infer First extends string,
  ...infer Rest extends string[]
]
  ? `${First}${AllCombinations<Rest>}` | AllCombinations<Rest>
  : "";
 
// 5-6'dan fazla elemanla bunu yapma

İleri Seviye Tipleri Ne Zaman KULLANMAMALI#

Bu belki de en önemli bölüm. İleri seviye tipler, hataları önlediğinde veya geliştirici deneyimini iyileştirdiğinde kullanılmalıdır. Gösteriş yapmak için kullanılmamalıdır.

Bunları şu durumlarda kullanma:

  • Basit bir Record<string, unknown> yeterli olacaksa
  • Tip yalnızca bir yerde kullanılıyorsa ve somut bir tip daha açık olacaksa
  • Tipi hata ayıklamaya, kurtaracağından daha fazla zaman harcıyorsan
  • Ekibin tipi okuyamıyor veya bakımını yapamıyorsa
  • Çalışma zamanı kontrolü daha uygun olacaksa

Bunları şu durumlarda kullan:

  • Birçok tipte tekrarlanan bir kalıbın varsa (mapped type'lar)
  • Bir fonksiyonun dönüş tipi girdisine bağlıysa (conditional type'lar)
  • Kendi kendini belgeleyen bir kütüphane API'si oluşturuyorsan
  • Yasadışı durumları temsil edilemez kılmak istiyorsan
  • Kod incelemelerinde aynı hata kategorisinin tekrar tekrar çıkmasından bıktıysan
typescript
// Aşırı mühendislik — basit bir config için bunu yapma
type Config = DeepReadonly<
  DeepRequired<
    Merge<DefaultConfig, Partial<UserConfig>>
  >
>;
 
// Sadece bunu yap
interface Config {
  readonly host: string;
  readonly port: number;
  readonly debug: boolean;
}

Temel kuralım: tipi açıklamak, önlediği hatayı açıklamaktan daha uzun sürüyorsa, basitleştir.

Karmaşık Tipleri Hata Ayıklama#

Karmaşık bir tip çalışmadığında, TypeScript'in neye çözdüğünü "görmek" için bu yardımcıyı kullanıyorum:

typescript
// IDE hover tooltip'lerinde inceleme için bir tipi genişletir
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};
 
// Hata ayıklamak için kullan
type Problematic = SomeComplexType<Input>;
type Debug = Prettify<Problematic>;
// Şimdi IDE'de Debug'ın üzerine gel ve çözümlenmiş tipi gör

& {} hilesi, TypeScript'i tip takma adını göstermek yerine tipi istekli bir şekilde değerlendirmeye zorlar. Beni saatlerce kafa karışıklığından kurtardı.

Bir başka teknik — izole et ve kademeli olarak test et:

typescript
// Bunun hepsini tek seferde hata ayıklamak yerine:
type Final = StepThree<StepTwo<StepOne<Input>>>;
 
// Parçalara ayır:
type AfterStep1 = StepOne<Input>;       // kontrol etmek için üzerine gel
type AfterStep2 = StepTwo<AfterStep1>;  // kontrol etmek için üzerine gel
type AfterStep3 = StepThree<AfterStep2>; // kontrol etmek için üzerine gel

Özet#

  • Conditional type'lar (T extends U ? X : Y) tipler için if/else'dir. Union'larla dağıtıcı davranışa dikkat et.
  • infer desen eşlemedir — fonksiyon imzaları, promise'ler ve diziler gibi yapılardan tipleri çıkarmak için kullan.
  • Mapped type'lar ({ [K in keyof T]: ... }) özellikler üzerinde yineleme yapar. Anahtar yeniden eşleme ve filtreleme için as ile birleştir.
  • Template literal type'lar stringleri tip seviyesinde manipüle eder. Mapped type'larla birleştirildiğinde, API tasarımı için inanılmaz güçlüdür.
  • Özyinelemeli tipler iç içe geçmiş yapıları işler ama derleyici patlamalarını önlemek için derinlik sınırlarına ihtiyaç duyar.
  • Ayrıştırılmış union'lar ve kapsamlı kontrol, TypeScript'teki en yüksek değerli tek kalıptır. Her yerde kullan.
  • Markalı tipler aynı temel tipi paylaşan değerlerin karışmasını önler. Uygulaması basit, etkisi yüksek.
  • Tipleri aşırı mühendislik yapma. Tip, önlediği hatadan daha zor anlaşılıyorsa, basitleştir. Amaç kod tabanını daha güvenli yapmak, tip golf yarışması kazanmak değil.

TypeScript tip sistemi Turing-complete'tir, yani onunla neredeyse her şeyi yapabilirsin. Sanat, ne zaman yapman gerektiğini bilmektir.

İlgili Yazılar