typescript - Tip ayrıştırması sırasında birleşim dağılımı neden 'çöker'?
Bu yaygın türü göz önünde bulundurun UnionToIntersection:
type UnionToIntersection<T> =
(T extends any ? (a: T) => void : never) extends (a: infer I) => void ? I : never;
Benim anladığım kadarıyla, ilk “adım”, TypeScript’in verilen birleşimi T karşılık gelen bir (a: T) => void birleşimi türüne dağıtmasını zorlar. Örneğin, (a: A) => void | (b: T) => void vb. Daha sonra, TypeScript’in fonksiyon birleşimlerini argüman olarak doğal şekilde nasıl işlediği nedeniyle, çıkarılan I T türlerinin kesişimi olur.
Örneğin, elimizde bu varsa:
type F1 = (arg: A) => void;
type F2 = (arg: B) => void;
const eitherF1orF2: F1 | F2 = (arg: any) => {}
Bizim eitherF1orF2 fonksiyonumuz, arg parametresinin A & B olmasını gerektirir.
Bu, fonksiyon birleştirmesinin oluşturulmasını ayrı bir adıma ayırdığımızda işe yaramaz:
type UnionOfFunctions<T> = T extends any ? (arg: T) => void : never
type UnionOfFunctionsToIntersection<T> = T extends (arg: infer I) => void ? I : never;
type UnionToIntersection<T> = UnionOfFunctionsToIntersection<UnionOfFunctions<T>>;
Bu ayrıştırılmış sürüm basitçe orijinal birleşimi döndürür.
Özellikle, bunları biraz farklı bir şekilde birleştirirsem, yine beklendiği gibi çalışıyor:
type UnionToIntersection<T> = UnionOfFunctions<T> extends (arg: infer I) => void ? I : never;
Bu davranış farkına neden olan hangi mekanizmayı veya özelliği gözden kaçırıyorum?
Cevap #1
UnionToIntersection'in iki versiyonunuz arasındaki fark, ara isimlendirilmiş tip takma adı olanın iç koşullu tipi dağıtılabilir bir koşullu tipe dönüştürmüş olmasıdır. Yani, tip
type UnionOfFunctionsToIntersection<T> = T extends (arg: infer I) => void ? I : never;
Dağıtacak birleşimlerin üzerinde T içinde, çünkü T çıplak bir jenerik tip parametresi. Bu yüzden UnionOfFunctionsToIntersection<X | Y | Z> hangi durumda olursa olsun zorunlu olarak UnionOfFunctionsToIntersection<X> | UnionOfFunctionsToIntersection<Y> | UnionOfFunctionsToIntersection<Z> olarak değerlendirilecektir:
type Z = UnionOfFunctionsToIntersection<F1 | F2> // A | B
O infer ifadesi baştan bir birlik (union) üzerinde hiç işlem yapmaz. Bu yüzden kontravaryant bir konumdan çıkarılan türlerin kesişimini üretme ihtimali yoktur.
Öte yandan, orijinal UnionToIntersection’in benzer bölümünde
type UnionToIntersection<T> =
(T extends any ? (a: T) => void : never) extends (a: infer I) => void ? I : never;
kontrol edilen tür (T extends any ? (a: T) => void : never) olup, saf bir tür parametresi değildir. Bu yüzden dağıtıcı bir koşullu tür değildir ve bu türde birleşimler üzerinde dağıtım yapmaz. Bu nedenle eğer bir birleşim üretirse, infer I bir birleşim üzerinde işlem yapacak ve karşı değişken konumundaki türlerden bir kesişimi çıkarma şansı olacaktır.
Bunu iki sürümün eşdeğer olması için düzeltmek üzere, UnionOfFunctionsToIntersection<T>'yi T üzerinde dağıtmaması için değiştirmelisiniz. Bunu yapmanın en kolay yolu, kontrolün her iki tarafını da bir tek-tuple içine sarmaktır, örneğin
type UnionOfFunctionsToIntersection<T> = [T] extends [(arg: infer I) => void] ? I : never;
type Z=UnionOfFunctionsToIntersection<F1 | F2>/A & B
Genel olarak, eğer senin bir türün varsa
type Foo<T> = Bar<T> extends ⋯ ? ⋯ : ⋯
ve onu yeniden düzenlemek istiyor
type Baz<T> = T extends ⋯ ? ⋯ : ⋯
type Foo<T> = Baz<Bar<T>>
ve sendika davranışıyla ilgileniyorsanız, ara tipinizin dağıtılabilir olmadığından emin olmalısınız:
type Baz<T> = [T] extends [⋯] ? ⋯ : ⋯
type Foo<T> = Baz<Bar<T>>
Tüm bunların, sizin eitherF1orF2 örneğinizde gösterildiği gibi, bir fonksiyonlar birleşimini çağırmaya çalıştığınızda TypeScript’in davranışıyla çok az ilgisi vardır:
type F1 = (arg: A) => void;
type F2 = (arg: B) => void;
declare const eitherF1orF2: F1 | F2;
declare const a: A;
eitherF1orF2(a); // error
// Argument of type 'A' is not assignable to parameter of type 'A & B'.
declare const b: B;
eitherF1orF2(b); // error
declare const ab: A & B;
// Argument of type 'B' is not assignable to parameter of type 'A & B'.
eitherF1orF2(ab); // okay
Burada argümanın tipi, parametrelerin kesişimi olmak zorundadır ve argümanı incelemek için IntelliSense kullandığınızda, bu kesişimi göreceksiniz. Ancak bu, infer'in koşullu tiplerde nasıl çalıştığıyla doğrudan eşleşmez. Evet, her iki davranış da birleşimleri kesişimlere dönüştüren kontravaryansı içerir, ama farklı bağlamlarda gerçekleşir. Yani, her iki özelliğin dayandığı temel tip teorisi aynıdır, ancak uygulanışları ilişkisizdir.
Gerçekten de, TypeScript 3.3 birleşim fonksiyonlarını çağırma desteğini getirmeden önce, eitherF1orF2 üzerine yapılan üç çağrı da başarısız olurdu çünkü F1 ve F2 farklı imzalara sahiptir. Hiçbir kesişim söz konusu değil, sadece hata vardı. Ancak TypeScript 2.8'de tanıtılan koşullu tip davranışı yine de bir kesişimi çıkarırdı.
Yani burada eitherF1orF2 davranışı biraz yanıltıcı; aslında UnionToIntersection'un nasıl çalıştığı (veya çalışmadığı) hakkında çok bir şey çıkarmak için onu gerçekten kullanamazsınız.
Koda yönelik oyun alanı bağlantısı
