typescript - any, unknown, {} tipleri arasındaki ilişkiler ve bunların diğer tiplerle ilişkilerini nasıl anlamalıyız?


Türler arasındaki ilişkileri anlamaya çalışırken bu koda sahibim

type CheckIfExtends<A, B> = A extends B ? true : false;

type T1 = CheckIfExtends<number, unknown>; //true
type T2 = CheckIfExtends<number, {}>; //true
type T3 = CheckIfExtends<number, any>; //true
type T4 = CheckIfExtends<() => void, unknown>; //true
type T5 = CheckIfExtends<() => void, {}>; //true
type T6 = CheckIfExtends<() => void, any>; //true
type T7 = CheckIfExtends<unknown, any>; //true
type T8 = CheckIfExtends<any, unknown>; //true
type T9 = CheckIfExtends<{}, unknown>; //true
type T10 = CheckIfExtends<{}, any>; //true
type T11 = CheckIfExtends<any, {}>; //boolean
type T12 = CheckIfExtends<unknown, {}>; //false

Oyun alanına bağlantı

Biri bunu açıklayabilir mi? Hangi fark? any extends {} olurken ve aynı zamanda any {}'i extend etmezken bu nasıl mümkün olabilir? Eğer any extends unknown ve unknown extends any ise, bu onların güçlü bir şekilde eşit olduğu anlamına mı geliyor? Bu, JavaScript'in null ve undefined eşitlik sorunlarının üstüne Typescript'in yeni bir hatası mı?

Aslında,

type T = CheckIfExtends<any, number>; //boolean

Cevap #1

Ayrım esasen şudur:

  • any türü kasıtlı olarak güvenilmezdir; çünkü hem herhangi bir türden hem de herhangi bir türe atanabilir (nerede kullandığınıza bağlı olarak never istisna olabilir). Güvenilmezlik, alt türlerin geçişliliği gibi türler için bazı temel kuralların bozulduğu anlamına gelir. Genel olarak, eğer A B'ye atanabiliyorsa ve B C'ye atanabiliyorsa, o zaman A C'ye atanabilir. Ancak any bunu bozar. Örneğin: string, any'ye atanabilir ve any, number'a atanabilir... ama string, number'a atanamaz. Bu özel güvenilmezlik çok kullanışlıdır, çünkü temelde tür denetimini doğru bir şekilde yazılması zor veya imkansız olan bir kod parçasında "kapamamıza" izin verir. Ama any'yi bir tür olarak düşünürken çok dikkatli olmalısınız; bu daha çok bir "typesız" gibidir.

  • boş tür, {}, çalışma zamanında bir nesne gibi davranılabilen bir türdür (yani, özelliklerini veya yöntemlerini çalışma zamanında hata almadan okuyabileceğiniz bir şeydir), ancak derleme zamanında bilinen hiçbir özelliği yoktur. Bu, özellikleri olmadığı anlamına gelmez; sadece derleyicinin bunlardan herhangi birini bilmediği anlamına gelir. Bu, yalnızca null ve undefined değerlerinin {} türüne atanamayacağı anlamına gelir (null.foo veya undefined.foo çalışma zamanı hatasıdır). string gibi ilkel türler bile çalışma zamanında özellikler ve yöntemler varmış gibi davranılabilir ("".length ve "".toUpperCase() çalışır, hatta "".foo sadece undefined döner). Ve elbette herhangi bir gerçek nesne türü de {} türüne atanabilir.

    Öte yandan, {} türü çok fazla türe atanamaz. Eğer elimde {} türünde bir değer varsa ve bunu {foo: string} türündeki bir değişkene atamaya çalışırsam, derleyici hatası alırım, çünkü {} içinde bir foo özelliği bulunduğu bilinmemektedir. {} kendisine veya unknown gibi daha geniş bir türe, ya da "türü olmayan" any'ye atanabilir.

    Bu, {}'yi tüm diğer türlerin atanabileceği bir tür olan neredeyse bir üst tür yapar. Temelde null ve undefined çıkarılmış bir üst türdür.

  • unknown türü TypeScript 3.0'da tanıtıldı ve gerçek üst türdür; TypeScript'teki her tür unknown'a atanabilir. Hatta null ve undefined bile unknown'a atanabilir.

    Öte yandan, unknown yalnızca kendisine ve "tipsiz" any'ye atanabilir. Hatta {} tipi bile unknown'u atamak için yeterince geniş değildir. Bunun yerine, unknown esasen {} | null | undefined ile eşdeğerdir (ve TypeScript 4.8'den itibaren böyle görülür).


Çoğu CheckIfExtends<A, B> sonucunuz yukarıdakilerle açıklanabilir. İstisna T11:

type T11 = CheckIfExtends<any, {}>; //boolean

CheckIfExtends<A, B> tür tanımınız dağıtılabilir bir koşullu türdür ve A bir birleşim türü olduğunda ilginç şeyler yapar; çünkü birleşimin parçaları her iki dalı da sağlıyorsa koşullu ifadenin her iki dalının da alınmasına izin verir. Ayrıca A any olduğunda da aynı dağıtımı yapar, B any veya unknown olmadıkça (bu nedenle T8 normal davranır). Bununla ilgili bazı tartışmalar microsoft/TypeScript#27418 numaralı konuda mevcuttur. Her neyse, T11 her iki dalı da alır ve bu yüzden true | false elde edilir, bu da boolean olur. (microsoft/TypeScript#27418'den; A konumundaki unknown dağıtılmaz, bu nedenle T7 ve T12 de normal davranır.)

Cevap #2

TypeScript’te extends ifadesinin ne anlama geldiğinden başlayacağım. İlk bakışta garip görünüyor; ürün türleri (nesneler gibi) için 'üstkümesi mi' gibi davranırken, birleşimler için 'altkümesi mi' gibi davranıyor ve fonksiyon türleri için tamamen farklı çalışıyor. İlk başta garip görünebilir, ama mantıklı bir davranış, başka bir deyişle çoğu türün sağlamlık (soundness) özelliği vardır.

Bu kavramı anlamak için genel kuralım, extends öğesini atanabilir olarak okumaktır. Eğer x y öğesini genişletiyorsa, bu y gerektiğinde x'in kullanılabileceği anlamına gelir.

Eğer yukarıdakiler onlar için geçerliyse, üç farklı cebirsel veri tipini ele alalım.

Ürün türü için

type A = {a: string}
type B = {a: string; b: number}
type BextendsA = B extends A ? true : false // evaluates to true 

Yukarıdaki doğru çünkü B, A'nın gerektiği her yerde kullanılabilir, çünkü B, A'nın sahip olduğu tüm yapıyı kapsar. B, A'nın bir üst kümesidir. Ama burada geçerli olan, B'nin A'ya atanabilir olmasıdır.

Birleşim türü için

type A = number | string
type B = number
type BextendsA = B extends A ? true : false // evaluates to true 

Birleşim için tamamen farklı görünüyor. Ürün için B bir üst küme idi, birleşim için B bir alt küme! Evet, farklı mantık, B A'daki tüm olası değerleri temsil etmiyor, ama her ne zaman A gerekliyse B onun yerine kullanılabilir. Yani B, A'ya atanabilir.

Fonksiyon türleri için

type A = (a: number) => void
type B = () => void
type BextendsA = B extends A ? true : false // evaluates to true 

Fonksiyon tipleri için, durum daha da garip görünüyor çünkü A, B'den daha belirli bir fonksiyon gibi görünüyor, o zaman B nasıl A'yı genişletebilir? Bu tekrar atama yeteneğinden kaynaklanıyor, çünkü bir şey A'ya ihtiyaç duyduğunda biz B'yi atayabiliriz. Bu, Array.map gibi örneklerde çok belirgindir. Düşünün:

[1,2].map(x => x + 1)

Array.map, üç argümanı olan bir fonksiyon gerektirir - (el,index,arr) => any ancak sadece bir argümana sahip bir fonksiyonla da çalışabilir el => any. Ve yine atanabilirlik geçerlidir, B A'ya atanabilir.

any, unknown, {} gibi türler güvenilmezdir, bu onların davranışlarının mantıksal olarak kanıtlanamayacağı anlamına gelir. Bunların TS'de nasıl davrandığını anlamak, daha çok spesifikasyonu anlamak ve bu tür kararların sebeplerini tartışmak demektir. Ama mantıksal olarak açıklanamazlar, çünkü güvenilmez türler mantığa aykırı davranır.

TypeScript'in güvenli olmayan davranışa izin verdiği yerler dikkatlice değerlendirildi ve bu belgede bu durumların nerede meydana geldiğini ve bunların ardındaki motivasyonel senaryoları açıklayacağız.

İlgili makaleler