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

TypeScript Utility Types 2: Exclude, Extract, NonNullable, ReturnType ve Diğerleri

Mert Özen 3 Kas 2026 8 dk 0 görüntülenme
TypeScript Utility Types 2: Exclude, Extract, NonNullable, ReturnType ve Diğerleri

Union'ları Exclude, Extract ve NonNullable ile süzüyor; ReturnType, Parameters, Awaited, InstanceType ve ConstructorParameters ile fonksiyon ve class'lardan tip türetiyoruz.

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

Önce Tahmin Et

Bir blog yazısının durumunu tutan bir union'ımız var. null, durumu henüz belirlenmemiş yazıları temsil ediyor:

type Status = "draft" | "published" | "archived" | null;

type A = Exclude<Status, "archived">;
type B = NonNullable<A>;
type C = Extract<Status, "draft" | "deleted">;

A, B ve C hangi type'lara karşılık gelir? Özellikle C'ye dikkat: "deleted", Status'un içinde yok. Bu bir hata mı?

Union'ları Süzmek: Exclude ve Extract

Bir önceki yazıdaki utility type'lar object'lerin property'leriyle uğraşıyordu. Bu yazının ilk grubu ise union'larla çalışır. Bir union'ı bir kova dolusu taş gibi düşün; Exclude ve Extract iki farklı elektir. Exclude<T, U>, T'nin üyelerinden U'ya uyanları atar ve gerisini bırakır. Extract<T, U> tam tersini yapar: sadece U'ya uyanları tutar.

Bu yüzden A, "draft" | "published" | null olur; "archived" elendi. C ise sadece "draft"'tır. Extract "ikinci listede olup birincide olmayanlar"la ilgilenmez; birinci union'ın her üyesine "sen bu kalıba uyuyor musun?" diye sorar. "deleted" diye bir üye olmadığı için sonuca hiçbir şey katmaz ve hata da vermez.

Süzgecin kalıbı bir literal olmak zorunda değil, bir object şekli de olabilir. Discriminated union'larda bu çok işe yarar:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rect"; width: number; height: number };

type Circle = Extract<Shape, { kind: "circle" }>;
// { kind: "circle"; radius: number }

type Polygon = Exclude<Shape, { kind: "circle" }>;
// { kind: "square"; size: number } | { kind: "rect"; width: number; height: number }

function area(circle: Circle): number {
  return Math.PI * circle.radius ** 2;
}

console.log(area({ kind: "circle", radius: 1 }).toFixed(2)); // 3.14

Extract<Shape, { kind: "circle" }> şunu sorar: union'ın hangi üyeleri { kind: "circle" }'a atanabilir? Sadece birincisi. Böylece union'ın tek bir üyesine, ona ayrıca isim vermeden ulaşırsın.

İçlerinde sihir yok. TypeScript'in kendi tanım dosyasında Exclude tek satırdır: type Exclude<T, U> = T extends U ? never : T;. Bu bir conditional type ve union'ın her üyesi için ayrı ayrı çalışıyor. Nasıl çalıştığını İleri serinin ilk yazısında açacağız.

NonNullable

NonNullable<T>, bir type'tan null ve undefined'ı atar. Baştaki B, "draft" | "published" olur. En sık kullanıldığı yer, optional ya da null olabilen bir property'nin "kontrol edilmiş" hâlini ifade etmektir. Daha önce gördüğümüz indexed access ile birlikte:

type User = {
  name: string;
  email: string | null;
  phone?: string;
};

type Email = NonNullable<User["email"]>; // string
type Phone = NonNullable<User["phone"]>; // string

function sendWelcome(email: Email) {
  console.log(`Welcome mail sent to ${email}`);
}

const user: User = { name: "Grace", email: "grace@example.com" };

if (user.email !== null) {
  sendWelcome(user.email); // Welcome mail sent to grace@example.com
}

sendWelcome, null ile uğraşmak istemiyor; kontrolü çağıran tarafa bırakıyor. if bloğunun içinde narrowing sayesinde user.email zaten string olduğu için çağrı derlenir. Merak edersen, güncel TypeScript sürümlerinde tanımı şaşırtıcı derecede kısadır: type NonNullable<T> = T & {};. {} "null ve undefined dışındaki her şey" anlamına geldiği için, intersection bu ikisini düşürür.

Fonksiyondan Tip Türetmek: ReturnType ve Parameters

İkinci grup bir fonksiyonun ya da class'ın tipinden yeni type'lar çıkarır. Bir fonksiyonun ne döndürdüğünü ayrı bir type olarak yazmak yerine, fonksiyonun kendisine sorabilirsin:

let nextId = 1;

function createUser(name: string, age: number) {
  return { id: nextId++, name, age, createdAt: new Date() };
}

type User = ReturnType<typeof createUser>;
// { id: number; name: string; age: number; createdAt: Date; }

type CreateUserArgs = Parameters<typeof createUser>;
// [name: string, age: number]

type UserName = Parameters<typeof createUser>[0]; // string

function createMany(rows: CreateUserArgs[]): User[] {
  return rows.map((args) => createUser(...args));
}

console.log(createMany([["Ada", 36], ["Alan", 41]]).map((u) => u.id)); // [ 1, 2 ]

// Hata: 'createUser' refers to a value, but is being used as a type here. Did you mean 'typeof createUser'?
type Wrong = ReturnType<createUser>;

Önce son satıra bakalım, çünkü herkes bir kez buna takılır. createUser bir değer, ReturnType ise bir type bekler. Değeri tip seviyesine taşımak için daha önce gördüğümüz typeof'u kullanırız: typeof createUser, fonksiyonun tipini, yani (name: string, age: number) => { ... } imzasını verir. ReturnType bu imzadan dönüş tipini, Parameters da parametreleri bir tuple olarak çeker.

Parameters'ın sonucu sıradan bir tuple değil, named tuple: [name: string, age: number]. Temel seride gördüğümüz bu isimler editörde ipucu olarak görünür. Bir tuple olduğu için de indexed access ile tek bir parametreye ulaşabilirsin: [0].

Bunlar en çok, tipini sen yazmadığın kodda işe yarar. Bir library'nin fonksiyonu ayarlarını bir object olarak alıyor ama o object'in type'ını dışarıya export etmiyorsa, Parameters<typeof libFunction>[0] sana tam olarak o type'ı verir. Kendi kodunda da tek kaynak ilkesi geçerli: createUser'a yeni bir alan eklediğinde User type'ı kendiliğinden güncellenir.

Awaited: Promise'in İçindeki Tip

async fonksiyonlarda ReturnType sana bir Promise verir. Çoğu zaman ise istediğin şey, await ettiğinde eline geçecek değerin tipidir. TypeScript 4.5 ile gelen Awaited tam olarak bunu yapar:

async function loadSettings() {
  return { theme: "dark", pageSize: 20 };
}

type Pending = ReturnType<typeof loadSettings>;
// Promise<{ theme: string; pageSize: number; }>

type Settings = Awaited<ReturnType<typeof loadSettings>>;
// { theme: string; pageSize: number; }

type Nested = Awaited<Promise<Promise<number>>>; // number
type Plain = Awaited<string>; // string

function render(settings: Settings) {
  console.log(`${settings.theme} / ${settings.pageSize}`);
}

loadSettings().then(render); // dark / 20

Awaited, await anahtar kelimesinin tip seviyesindeki karşılığıdır. İç içe Promise'leri sonuna kadar açar, Promise olmayan bir type'ı da olduğu gibi bırakır; tıpkı await 5'in 5 vermesi gibi. Awaited<ReturnType<typeof fn>> kalıbını sık göreceksin. Asenkron kodun tiplerini ayrıca konuşacağız; şimdilik bu kalıbı tanıman yeterli.

Class'lardan Tip Türetmek: InstanceType ve ConstructorParameters

Daha önce class'ları anlatırken bir inceliğe değinmiştik: bir class'ın adı, tip olarak kullanıldığında instance'ın tipini ifade eder. typeof HttpClient ise class'ın kendisinin, yani constructor fonksiyonunun tipidir. Bu iki utility type ikincisiyle çalışır:

class HttpClient {
  baseUrl: string;
  timeoutMs: number;

  constructor(baseUrl: string, timeoutMs: number) {
    this.baseUrl = baseUrl;
    this.timeoutMs = timeoutMs;
  }
}

type ClientArgs = ConstructorParameters<typeof HttpClient>;
// [baseUrl: string, timeoutMs: number]

type Client = InstanceType<typeof HttpClient>; // HttpClient

function create<T extends new (...args: any) => any>(
  ctor: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new ctor(...args);
}

const client = create(HttpClient, "https://api.example.com", 3000);
console.log(client.timeoutMs); // 3000

// Hata: Expected 3 arguments, but got 2.
create(HttpClient, "https://api.example.com");

// Hata: Type 'HttpClient' does not satisfy the constraint 'abstract new (...args: any) => any'.
type WrongClient = InstanceType<HttpClient>;

Doğrudan bir class için InstanceType<typeof HttpClient> yazmak gereksiz; HttpClient zaten aynı type. Asıl değeri generic kodda ortaya çıkar. create fonksiyonu hangi class'ı alacağını bilmiyor, ama aldığı class'ın constructor parametrelerini ve üreteceği instance'ın tipini çıkarabiliyor. Eksik argümanla çağırdığında hata, sanki constructor'ı doğrudan çağırmışsın gibi gelir.

Constraint'teki new (...args: any) => any "herhangi bir constructor" demenin yolu; TypeScript'in kendi tanımları da aynı kalıbı kullanır. Son satırdaki hata ise typeof'u unutmanın class versiyonu: HttpClient bir instance type'ı, constructor değil.

Sık Düşülen Tuzaklar

type Status = "draft" | "published" | "archived";
type Visible = Exclude<Status, "archivd">; // derlenir, hiçbir şey elenmez

function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
  return typeof input === "string" ? Number(input) : String(input);
}

type Parsed = ReturnType<typeof parse>; // string
type ParseArgs = Parameters<typeof parse>; // [input: number]

function identity<T>(value: T): T {
  return value;
}

type Id = ReturnType<typeof identity>; // unknown

Birincisi, bir önceki yazıda Omit'te gördüğümüz sorunun aynısı. Exclude'un ikinci parametresi hiçbir şeyle kısıtlı değil; "archivd" gibi bir yazım hatası sessizce geçer ve Visible bütün Status olarak kalır.

İkincisi overload'lar. Bir fonksiyonun birden fazla overload imzası varsa, ReturnType ve Parameters sadece sonuncusuna bakar. parse string alınca number döndürüyor, ama ReturnType sana yalnızca string der. Overload'lu fonksiyonlarda bu yardımcılara güvenme; ihtiyacın olan type'ı açıkça yaz.

Üçüncüsü generic fonksiyonlar. typeof identity henüz bir T ile çağrılmamış bir imzadır. ReturnType onu açarken T'nin yerine en genel ihtimali, unknown'u koyar. Belirli bir çağrının sonucunu istiyorsan o çağrının sonucunu bir değişkene atayıp typeof ile al.

Küçük Bir Deneme

★☆☆ type Primitive = string | number | boolean | bigint | symbol | null | undefined; için önce sadece sayısal type'ları (number | bigint) Extract ile, sonra null ve undefined dışındaki her şeyi NonNullable ile türet.

★★☆ Tahmin et: async function getIds() { return [1, 2, 3]; } için Awaited<ReturnType<typeof getIds>>[number] hangi type'tır? Sonra Playground'da üzerine gelerek doğrula.

★★★ Bir fonksiyonu alıp, onunla aynı parametreleri kabul eden ve aynı değeri döndüren, ama çağrılmadan önce argümanları konsola yazan generic bir withLogging fonksiyonu yaz. İpucu: T extends (...args: any) => any, Parameters<T> ve ReturnType<T>.

Bu iki yazıda type'ları hep derleme zamanında dönüştürdük. Bir sonraki yazıda çalışma zamanına dönüyoruz: bir değerin gerçekten hangi type'ta olduğunu kontrol eden fonksiyonları, yani type guard'ları yazacağız. x is T yazımıyla tanışacak ve TypeScript 5.5'in bu işi bizim yerimize nasıl yapabildiğini göreceğiz.