Typescript iç içe geçmiş tiplerin birleşimi, tipler arasında paylaşılmayan anahtarı kaybediyor
Bunun Typescript ile aynı çizgide olabileceğine inanıyorum - birleşim tipleri, TypeScript tipleme sorunu ve Typescript özelliği birleşim tipinde mevcut değil ama hiçbiri tam olarak aynı değil. Object.entries...map ile bir birleşim tipi oluşturulduğunda, paylaşılmayan anahtarları kaldırıyor gibi görünüyor. İşte bazı örnek kodlar:
type Params = {
head1:{
sub1:{
a:string
}},
head2:{
sub2:{
a:string,
b:number
}}}
(params:Params)=>{
Object.entries(params).map(([headkey, sub])=>{
Object.entries(sub).map(([subkey, subval])=>{
subval.a // <- Works fine
if('b' in subval)
subval.b // <- Error, "Property 'b' does not exist on type '{ a: string; }'"
})
})
}
Oyun alanı
Bunu aşmanın bir yolu var mı? Bu bir hata mı?
Cevap #1
Bu bir hata değil, ancak bu durumda hoşunuza gitmeyen etkileri olabilir. Derleyicinin birden fazla türden tek bir tür çıkarması gerektiğinde, "en iyi ortak tür" olarak adlandırılan şeyi sentezlemeye çalışır. Çoğu zaman bu, ilgili tüm türlerin doğrudan bir birleşimi olacaktır:
interface X { x: string }
declare const x: X;
interface Y { y: string; }
declare const y: Y;
const eitherXY = Math.random() < 0.5 ? x : y
// const eitherXY: X | Y
Ama bazen ilgili türlerden en az biri diğer ilgili türlerden birine atanabilir olacak ve bu durumlarda derleyici bir birleşim yerine sadece tek, daha genel türü çıkarır:
interface Z { x: string, z: number }
declare const z: Z;
const eitherXZ = Math.random() < 0.5 ? x : z
// const eitherXZ: X // <-- this is X, not X | Z
Bu işe yarıyor çünkü ilan edilmemiş olmasına rağmen, Z extends X. TypeScript'te nesne tipleri, yapısal alt tipleme nedeniyle özellik ekleme yoluyla genişletilebilir. Bu yüzden derleyici, X | Z yerine sadece X olarak çıkarsamaya karar veriyor.
Artık TypeScript'in yapısal alt türlemeye dayalı tamamen doğru bir tür sistemi olduğu bir dünyada, Z extends X ise, birleşim X | Z ile sadece X arasında gözlemlenebilir bir fark olmayabilir. Sonuçta, türleri bu türe atanabilir tüm JavaScript değerlerinin kümesine karşılık geldiğini düşünebiliriz. Eğer Z extends X ise, Z türündeki her değer aynı zamanda X türünde bir değerdir ve dolayısıyla X | Z ile X aynı değer kümesine karşılık gelir. Bu dünyada, X | Z ile sadece X seçimi tamamen türün nasıl görüntülendiği ile ilgili olurdu.
Ama biz o dünyada yaşamıyoruz.
Bir yer X | Z, X'den gözle görülür şekilde farklıdır, bu da nesne literalleri için fazladan özellik kontrolü yapıldığında görülür:
const xOrZ: X | Z = { x: "hello", z: 123 }; // okay
const justX: X = { x: "hello", z: 123 }; // error, excess property
Fazla özellik uyarıları, onları tür hatası olarak düşünürseniz teknik olarak yanlış pozitiflerdir, bu yüzden belki de onları bir linter uyarısı olarak düşünmek daha iyidir.
Başka bir yer, in operatörünü bir tür koruyucu (type guard) olarak kullandığınız zamandır:
declare const x: X;
if ("z" in x) {
x.z.toFixed(); // error
}
declare const xz: X | Z;
if ("z" in xz) {
xz.z.toFixed(); // okay
}
Bunun teknik olarak yanlış olduğu bilinmektedir. Ne de olsa, {x: "", z: ""} değeri X türünün geçerli bir değeri olmalı ve dolayısıyla X | Z türünün geçerli bir değeri olmalıdır:
const hmm = { x: "", z: "" }
const alsoX: X = hmm; // okay
const alsoXZ: X | Z = hmm; // okay
Bu nedenle, sadece bir z özelliğinin varlığının onun bir number olduğunu ima ettiğini düşünmek gerçekten bir hata olmalıdır. Yukarıdaki in tür korumalarının hiçbiri, sıkı davransaydık, çalışmamalı. Ama pratikte, ini bir tür koruması olarak kullanabileceğinizi varsaymak çok yararlıdır (ve insanlar genellikle daha fazlasını ister; sadece xz.z kullanmak isterler, önce "z" in xz kontrol etmeden, ancak bu her zaman reddedilmiştir).
Tüm bunlar, yapısal alt tipleme ve birleşimler hakkında nasıl düşündüğünüze ve bunun derleyicinin bir tür veya ifadeyi nasıl ele aldığıyla uyuşup uyuşmadığına bağlı olarak, muhtemelen doğru ama rahatsız edici uyarılar veya yanlış ama faydalı uyarı eksiklikleri alacağınız anlamına gelir... bunların hiçbiri hata değildir.
Yani burada bir hata yok.
Bunu söyledikten sonra, bunu aşmak için ne yapabilirsiniz? Sizin durumunuzda sorun, Object.entries() fonksiyonunun aşağıdaki çağrı imzasına sahip olmasıdır:
/* (method) ObjectConstructor.entries<T>(o: {
[s: string]: T;
} | ArrayLike<T>): [string, T][] (+1 overload) */
Ve bunu sub üzerinde çağırdığınızda, derleyici T'yi {a: string} olarak çıkarır, bu da {a: string} ve {a: string, b: number} için en iyi ortak üst türdür:
Object.entries(sub)
/* (method) ObjectConstructor.entries<{ a: string; }>(...) */
Bu nedenle, bunun etrafından dolaşmanın en basit yolu, T'yi beklediğiniz birleşim tipi olarak açıkça belirtmektir. Bunu ya {a: string} | {a: string, b: number} olarak yazabilirsiniz, ya da derleyicinin bu tipi Params cinsinden hesaplamasını sağlayabilirsiniz:
type SubSubParams =
{ [K in keyof Params]: Params[K][keyof Params[K]] }[keyof Params]
/* type SubSubParams = {
a: string;
} | {
a: string;
b: number;
} */
ve sonra
Object.entries<SubSubParams>(sub).map(([subkey, subval]) => {
subval.a
if ('b' in subval)
subval.b // okay
})
Koda yönelik oyun alanı bağlantısı
