TypeScript conditional types, A extends B ? X : Y biçiminde yazılan ve bir tipin başka bir tipe atanabilir olup olmamasına göre iki sonuçtan birini seçen tiplerdir. Sol taraftaki tip sağdakine atanabiliyorsa "doğru" dal, atanamıyorsa "yanlış" dal sonuç olur. infer anahtar kelimesiyle de karşılaştırılan tipin içinden yeni bir tip yakalayabilirsiniz.
Bu yazıda conditional types'ın temel sözdizimini, doğru dalda tipin nasıl daraldığını, infer kullanımını, union'lar üzerinde dağılma (distributive) davranışını ve özyinelemeli tipleri TypeScript 6.0 ile denenmiş örneklerle anlatıyoruz. TypeScript ileri seviye özeti konuya kısa bir giriş verir; burada mekanizmanın ayrıntılarına iniyoruz.
Temel sözdizimi

Conditional type, JavaScript'teki üçlü operatöre (kosul ? a : b) benzer ama değerlerle değil tiplerle çalışır:
interface Hayvan {
yasa(): void;
}
interface Kopek extends Hayvan {
havla(): void;
}
type Ornek1 = Kopek extends Hayvan ? number : string; // number
type Ornek2 = RegExp extends Hayvan ? number : string; // stringSomut tiplerle yazıldığında pek işe yaramıyor gibi görünür; sonucu zaten biliyoruz. Gücü, generics ile birleştiğinde ortaya çıkar.
Overload yerine conditional type
Girdinin tipine göre farklı bir tip döndüren bir fonksiyon düşünün. Overload ile her durum için ayrı imza yazmanız gerekir ve desteklenen tip sayısı arttıkça imza sayısı da hızla büyür. Aynı mantığı tek bir conditional type ile ifade edebilirsiniz:
interface IdEtiket {
id: number;
}
interface AdEtiket {
ad: string;
}
type AdVeyaId<T extends number | string> = T extends number ? IdEtiket : AdEtiket;
declare function etiketOlustur<T extends number | string>(deger: T): AdVeyaId<T>;
const e1 = etiketOlustur("typescript"); // AdEtiket
const e2 = etiketOlustur(2.8); // IdEtiket
const e3 = etiketOlustur(Math.random() ? "merhaba" : 42); // IdEtiket | AdEtiketÜçüncü çağrıda argümanın tipi "merhaba" | 42 bir union'dır; sonuç da her iki dalın union'ı olur. Bunun sebebi aşağıda anlatacağımız dağılma davranışıdır. Overload yazımını fonksiyon tipleri ve overload yazımızda bulabilirsiniz.
Doğru dalda tip daralır
Bir koşulu kontrol ettiğinizde, doğru daldaki tip parametresi kontrol ettiğiniz tiple kısıtlanmış sayılır. Narrowing ile değerlerin daralmasına benzer. Önce kısıtsız bir sürümün neden hata verdiğine bakalım:
type MesajTipi<T> = T["mesaj"];
// TS2536: Type '"mesaj"' cannot be used to index type 'T'.T'nin mesaj özelliği olduğu bilinmiyor. Kısıtı dışarı alıp bir conditional type içine koyarsanız hem her tipi kabul edersiniz hem de doğru dalda T["mesaj"] güvenle okunur:
type MesajTipi<T> = T extends { mesaj: unknown } ? T["mesaj"] : never;
interface Eposta {
mesaj: string;
}
interface Kedi {
miyavla(): void;
}
type A = MesajTipi<Eposta>; // string
type B = MesajTipi<Kedi>; // neverAynı fikirle bir dizinin eleman tipini çıkaran, dizi olmayanı olduğu gibi bırakan bir tip yazılabilir. Doğru dalda T artık bir dizi sayıldığı için T[number] kullanılabilir:
type Duzlestir<T> = T extends any[] ? T[number] : T;
type S = Duzlestir<string[]>; // string
type N = Duzlestir<number>; // numberinfer ile tip yakalamak

Yukarıda eleman tipini T[number] ile "elle" çıkardık. infer anahtar kelimesi bunu daha bildirimsel yapar: extends tarafındaki desende bir yere infer Ad yazarsınız, derleyici o konuma denk gelen tipi yakalar ve doğru dalda Ad olarak kullanmanıza izin verir:
type Duzlestir<T> = T extends Array<infer Oge> ? Oge : T;
type DonusTipi<T> = T extends (...args: never[]) => infer D ? D : never;
type R1 = DonusTipi<() => number>; // number
type R2 = DonusTipi<(x: string) => string>; // string
type R3 = DonusTipi<(a: boolean, b: boolean) => boolean[]>; // boolean[]
type PromiseIci<T> = T extends Promise<infer U> ? U : T;
type R4 = PromiseIci<Promise<string>>; // stringYerleşik ReturnType, Parameters ve benzeri tipler de bu teknikle yazılmıştır. Tamamı için utility types rehberine bakabilirsiniz.
Overload'larda son imza
Birden fazla çağrı imzası olan bir tipten (örneğin overload'lı bir fonksiyondan) çıkarım yapıldığında yalnızca son imza dikkate alınır. Argüman listesine göre overload seçimi yapılamaz:
declare function cevir(x: string): number;
declare function cevir(x: number): string;
declare function cevir(x: string | number): string | number;
type T1 = ReturnType<typeof cevir>; // string | numberAynı değişken için birden fazla infer
Aynı infer değişkeni desende birden fazla yerde geçebilir. TypeScript 2.8 sürüm notlarına göre kovaryant konumlardaki (örneğin özellik tipleri) adaylar union olarak, kontravaryant konumlardaki (fonksiyon parametreleri) adaylar intersection olarak birleşir:
type Ortak<T> = T extends { a: infer U; b: infer U } ? U : never;
type O1 = Ortak<{ a: string; b: string }>; // string
type O2 = Ortak<{ a: string; b: number }>; // string | number
type Param<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type P1 = Param<{ a: (x: string) => void; b: (x: number) => void }>; // neverP1 sonucunun never olmasına dikkat edin: kontravaryant adaylar string & number kesişimine birleşir ve hiçbir değer aynı anda hem string hem number olamayacağı için bu kesişim never'a indirgenir.
infer ile extends kısıtı (TypeScript 4.7)
Yakaladığınız tipi ayrıca kontrol etmek için eskiden iç içe ikinci bir conditional type gerekirdi. TypeScript 4.7'den beri infer değişkenine doğrudan kısıt yazabilirsiniz:
type IlkString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = IlkString<[string, number]>; // string
type B = IlkString<["merhaba", boolean]>; // "merhaba"
type C = IlkString<[boolean, string]>; // neverDistributive conditional types
Kontrol edilen tip "çıplak" bir tip parametresiyse (yani T extends ... biçimindeyse) ve bu parametreye bir union verilirse, conditional type union'ın her üyesine ayrı ayrı uygulanır ve sonuçlar yeniden union olarak birleşir. Buna dağılma (distribution) denir:
type DiziYap<T> = T extends any ? T[] : never;
type D1 = DiziYap<string | number>;
// string[] | number[]
// Adımlar: DiziYap<string> | DiziYap<number>
type StringMi<T> = T extends string ? true : false;
type D2 = StringMi<string | number>; // boolean (true | false)
type D3 = DiziYap<never>; // neverSon satır sık gözden kaçar: never boş bir union gibi davranır. Dağıtılacak hiçbir üye olmadığı için sonuç da never olur.
Dağılma çoğu zaman istenen davranıştır; yerleşik Exclude ve Extract bu sayede çalışır. Kapatmak isterseniz extends'in iki tarafını köşeli parantezle sararsınız; artık tip parametresi çıplak olmadığı için union bir bütün olarak değerlendirilir:
type DiziYapDagitmadan<T> = [T] extends [any] ? T[] : never;
type D4 = DiziYapDagitmadan<string | number>; // (string | number)[]
// Yerleşik Exclude ile aynı mantık:
type Haric<T, U> = T extends U ? never : T;
type D5 = Haric<"a" | "b" | "c", "a">; // "b" | "c"| Yazım | string | number için sonuç | Dağılır mı? |
|---|---|---|
| T extends any ? T[] : never | string[] | number[] | Evet |
| [T] extends [any] ? T[] : never | (string | number)[] | Hayır |
| T extends string ? true : false | boolean | Evet |
| [T] extends [string] ? true : false | false | Hayır |
Özyinelemeli conditional types
TypeScript 4.1'den beri bir conditional type kendi dallarında kendisine başvurabilir. Böylece iç içe dizileri sonuna kadar açan bir tip yazılabilir:
type DerinEleman<T> = T extends ReadonlyArray<infer U> ? DerinEleman<U> : T;
type E1 = DerinEleman<number[]>; // number
type E2 = DerinEleman<number[][][]>; // number
type E3 = DerinEleman<[string, [boolean]]>; // string | booleanYerleşik Awaited de iç içe Promise'leri benzer bir özyinelemeyle açar. Özyinelemeli tipler derleyiciye ek iş yükler; çok derin ya da çok geniş tiplerde derleme yavaşlayabilir ve derleyici bir derinlik sınırına ulaştığında hata verebilir.
Mapped types ile birlikte
Conditional types, mapped types ile birleşince nesne anahtarlarını değer tiplerine göre süzebilirsiniz. TypeScript 2.8 sürüm notlarındaki kalıba benzer bir örnek: yalnızca fonksiyon olan alanların adlarını bulmak.
type FonksiyonAlanlari<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Parca {
id: number;
ad: string;
guncelle(yeniAd: string): void;
}
type F = FonksiyonAlanlari<Parca>; // "guncelle"
type Veri = Omit<Parca, FonksiyonAlanlari<Parca>>; // { id: number; ad: string; }Sık yapılan hatalar

Dağılmayı beklememek
StringMi<string | number> sonucunun false değil boolean çıkması çoğu kişiyi şaşırtır. Union'ı bütün olarak test etmek istiyorsanız köşeli parantez kalıbını kullanın.
Fonksiyon gövdesinde conditional dönüş tipi
Dönüş tipi henüz çözülmemiş bir conditional type ise, derleyici gövdedeki return ifadelerini bu tiple eşleştiremeyebilir:
interface IdEtiket { id: number; }
interface AdEtiket { ad: string; }
type AdVeyaId<T extends number | string> = T extends number ? IdEtiket : AdEtiket;
function etiket<T extends number | string>(x: T): AdVeyaId<T> {
if (typeof x === "number") {
return { id: x };
// TS2322: Type '{ id: number; }' is not assignable to type 'AdVeyaId<T>'.
}
return { ad: x as string };
// TS2322: Type '{ ad: string; }' is not assignable to type 'AdVeyaId<T>'.
}Bu durumda yaygın çözümler, dışarıya conditional imzayı gösteren bir overload yazıp uygulamayı daha geniş bir imzayla yapmak ya da dönüşte bilinçli bir tip dönüşümü kullanmaktır. Handbook örneklerinde de bu tür fonksiyonlar gövdesiz (declare) imzalarla gösterilir.
infer'ı yanlış yerde kullanmak
infer yalnızca bir conditional type'ın extends tarafında kullanılabilir; normal bir tip parametresinin kısıtında yazılamaz.
Sık Sorulan Sorular
extends burada kalıtım mı demek?
Hayır. Conditional type içindeki extends "atanabilir mi?" sorusudur. Sol taraftaki tipin bir değeri sağdaki tipe atanabiliyorsa koşul doğrudur; sınıf kalıtımıyla doğrudan ilgisi yoktur.
never neden bu kadar sık çıkıyor?
never union'lardan sessizce düşer. Bu yüzden "bu üyeyi at" anlamında yanlış dalda never döndürmek yaygın bir kalıptır. never tipinin genel davranışını any, unknown ve never yazımızda anlatıyoruz.
Conditional types çalışma anında bir şey yapar mı?
Hayır. Tamamen derleme zamanında değerlendirilir ve JavaScript çıktısından silinir. Çalışma anında karar vermek için typeof, instanceof gibi gerçek kontroller yazmanız gerekir.
Template literal tiplerde infer kullanılabilir mi?
Evet. S extends `${infer A}.${infer B}` gibi desenlerle string literal tipleri parçalayabilirsiniz. Örnekleri template literal types yazımızda bulabilirsiniz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: TypeScript öğrenme rehberi · Generics · Mapped types · Utility types · Template literal types · TypeScript ileri seviye özeti