TypeScript birleşim tipleri - yalnızca fonksiyon çağrısında hata
TypeScript'te birleşim türleri kullanırken garip bir davranışla karşılaştım. Bir işlevin referansını çağırmadan geçirdiğinizde, herhangi bir tür hatası oluşmuyor:
type A = boolean;
type B = string;
type C = number;
type BC = B | C;
interface Props { onChange(expr: A | BC): void }
const SomeFunction = (_props: Props) => {}
const handler = (el: A | B) => { console.log(el) }
SomeFunction({onChange: handler}) // no error here
SomeFunction({onChange: e => handler(e)}) // <- TS Error: Argument of type 'boolean | BC' is not assignable to parameter of type 'string | boolean'.
Bulabildiğim tek açıklama, TypeScript 3.3'te birleştirme türlerini çağırma davranışının iyileştirilmiş olmasıdır.
Bir hata olmadan SomeFunction({onChange: handler}) çağrısının yapılmasının nedenleri hakkında merak ediyorum - Bu, birleşim tipleri arasında bir örtüşme olduğu için mi?
TypeScript oyun alanına bağlantı
Cevap #1
Fark şudur ki handler bir yöntem olmayan fonksiyondur, oysa onChange Props içinde bir yöntem olarak ilan edilmiştir. Ve --strictFunctionTypes derleyici seçeneğini etkinleştirdiğinizde (bu, --strict derleyici işlevselliği paketinin bir parçasıdır), yöntem olmayan fonksiyonların parametreleri daha sıkı bir şekilde kontrol edilir.
Tip güvenli olmak için, bir fonksiyonun parametreleri kontravarient olarak kontrol edilmelidir, bu bu soru/cevapta açıklanmıştır ama kısaca, bir fonksiyon parametresi güvenli bir şekilde daraltılabilir ancak genişletilemez. Expr | Primitive, Expr | StringOrBool'den daha geniş olduğu için, burada bazı işlevler eklediğinizde gösterildiği gibi, handler'ı bir onChange olarak değerlendirmek güvenli değildir:
const SomeFunction = (_props: Props) => { _props.onChange(3) }
const handler = (el: Expr | StringOrBool) => {
if ((typeof el !== "boolean") && (typeof el !== "object")) {
console.log(el.toUpperCase()); // has to be a string, right?
}
console.log(el)
}
SomeFunction({ onChange: handler }) // runtime error: el.toUpperCase is not a function
handler fonksiyonu, boolean veya nesne olmayan bir parametrenin bir string olması gerektiğini varsayar, ancak SomeFunction, _props.onChange() fonksiyonunu bir sayı ile çağırır. Hata!
Bu noktada, eğer tek önemsediğimiz şey tür güvenliği ise, SomeFunction çağrılarının her ikisinde de bir hata olması gerektiği açık olmalıdır. Gerçekten de, Props'u onChange için yöntem dışı bir sözdizimi kullanacak şekilde yeniden yazarsak:
interface Props {
onChange: (expr: Expr | Primitive) => void // call expression, not method
}
o zaman her iki hatayı da görürüz:
SomeFunction({ onChange: handler }) // error!
SomeFunction({ onChange: e => handler(e) }) // error!
Ancak yöntemler (ve aslında --strictFunctionTypes etkin değilken tüm fonksiyonlar) hem güvenli daraltmayı (kontravarans) hem de güvensiz genişletmeyi (kovarans) izin verir. Yani, yöntem parametreleri biyvariant olarak kontrol edilir. Neden? Aslında, bu güvensiz olmasına rağmen oldukça kullanışlıdır. Ayrıntılar için bu SSS maddesini veya bu dökümantasyonu okuyabilirsiniz, ama kısaca, bazı yaygın kodlama pratiklerini (örneğin olay işleme) mümkün kılar. İnsanlar seviyor bir Dog[] dizisini, teknik olarak güvensiz olmasına rağmen bir Animal[] gibi değerlendirmeyi. Eğer yöntemler güvenli davranıyor olsaydı, o zaman Animal[] dizisinin push() metodunun varlığı, hiçbir kişinin bir Dog[] dizisini bir Animal[] olarak kullanmasını engellerdi. Daha fazla bilgi için bu Soru/Cevap bölümüne bakabilirsiniz.
Hem yöntem sözdizimine hem de ok fonksiyonu sözdizimine sahip olduğumuz için, istersek her iki şekilde de yapabiliriz; fonksiyon parametrelerinin tür güvenliğiyle ilgilendiğiniz her zaman, yöntem sözdiziminden uzak durun.
Koda yönelik oyun alanı bağlantısı
Cevap #2
Bunun için bir çözüm, katı birleşimler kullanmak olabilir:
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>>
: never;
type StrictUnion<T> = StrictUnionHelper<T, T>;
O zaman eğer Primitive tipini type Primitive=StrictUnion<StringOrBool | Num> olarak tanımlarsanız, her ikisi için de doğru hatayı alırsınız. Bence bu, @jcalz'in yorumlarda bahsettiği, method syntax declaration yönteminin bu şekilde desteklenmediği sorun için bir çözüm yolu. StrictUnion ayrıca method syntax'ı da anlıyor. Bu implementasyondan burada bahsediliyor.
