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

Temel sözdizimi

Conditional type, JavaScript'teki üçlü operatöre (kosul ? a : b) benzer ama değerlerle değil tiplerle çalışır:

typescript
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; // string

Somut 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:

typescript
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:

typescript
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:

typescript
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>;   // never

Aynı 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:

typescript
type Duzlestir<T> = T extends any[] ? T[number] : T;

type S = Duzlestir<string[]>; // string
type N = Duzlestir<number>;   // number

infer ile tip yakalamak

infer ile tip yakalamak: Overload'larda son imza, Aynı değişken için birden fazla infer, infer ile extends kısıtı (TypeScript 4.7)

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:

typescript
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>>; // string

Yerleş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:

typescript
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 | number

Aynı 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:

typescript
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 }>; // never

P1 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:

typescript
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]>;     // never

Distributive 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:

typescript
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>;            // never

Son 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:

typescript
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ımstring | number için sonuçDağılır mı?
T extends any ? T[] : neverstring[] | number[]Evet
[T] extends [any] ? T[] : never(string | number)[]Hayır
T extends string ? true : falsebooleanEvet
[T] extends [string] ? true : falsefalseHayı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:

typescript
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 | boolean

Yerleş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.

typescript
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

Sık yapılan hatalar: Dağılmayı beklememek, Fonksiyon gövdesinde conditional dönüş tipi, infer'ı yanlış yerde kullanmak

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:

typescript
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