Koşullu dalda Typescript daralmasını önleyin


Görünüşe göre Typescript, kontrol akışı analizi yaparken bir birleşimi daraltıyor:

type Foo = {x: number}
type Bar = {x: number, y: string}


function f(arg: Foo | Bar) {

    // Type of a is Foo | null
    const a = true ? arg : null
}

a tipinin Foo | null olduğunu varsayıyorum çünkü Foo, Bar'ın bir alt türüdür ve bu nedenle burada en az güvenli tip budur. Bu daralmayı nasıl önleyebilirim? Örneğin, aşağıdakileri yapmak istesem ne olur:

function p(arr: (Foo | Bar)[]) {

    const b = arr.length ? arr[0] : null;

    // Treat b as Foo | Bar | null
    if (b && 'y' in b) {
        
        // Error: Property 'y' does not exist on type 'never'
        console.log(b.y);
    }
}

Cevap #1

Her Bar aynı zamanda bir Foo olduğu için, birleşim Foo | Bar, hangi değerlerin atanabileceği açısından sadece Foo ile eşdeğerdir. Bir bakıma, Foo | Bar sadece Foo kadar bilgi içerir, tıpkı string | "hello" ifadesinin sadece string ile aynı bilgiyi içermesi gibi.

Derleyici Foo | Bar türünü açıkça Foo türüne dönüştürdüğünde, buna "alt tür indirgeme" denir (microsoft/TypeScript#4537'de uygulandığı gibi). Bu doğrudur, ancak bazen can sıkıcı olabilir çünkü teknik olarak | Bar, orada hangi tür değerlerin olabileceği hakkında size ekstra bilgi vermez; pratik açıdan ise, y özelliğinin ne olduğunu belirlemek için kullanılabileceğine dair bir ipucu verir.


in operatörü, bir özelliğin varlığının birleşim tipini daraltmak için kullanıldığı bir tür koruyucu olarak ele alınır (microsoft/TypeScript#15256'da uygulandığı gibi). Bu faydalı ama mantıksal olarak güvenilir değildir. Pratik olarak konuşursak, eğer bir {a: 0} | {b: 1} bir a özelliği içeriyorsa, bunun bir {a: 0} olduğunu beklersiniz, ancak teknik olarak bunu varsayamazsınız. Sonuçta, {a: "oops", b: 1} değeri geçerli bir {a: 0} | {b: 1} iken, geçerli bir {a: 0} değildir. Yalnızca bir a özelliğinin varlığı, teknik olarak tipi daraltmak için yeterince bilgi vermez, ancak pratik olarak verir.


Özellikleri ve birleşimleri işleme konusundaki bu iki yol birbirleriyle çelişiyor ve sizin durumunuzda kötü bir şekilde etkileşiyorlar.

İlk olarak Foo | Bar, Foo'ya indirgenir (teknik olarak doğru), ancak bunun y özelliğine sahip olduğu bilinmemektedir. Daha sonra bir y özelliğini kontrol etmek için in operatörünü kullanırsınız, bu da bilinen bir y özelliğinin eksikliği nedeniyle bunun hiçbir şekilde bir Foo olamayacağına karar verir (teknik olarak yanlış).

microsoft/TypeScript#37518 adlı bir sorun var ve bu tıpkı bu gibi görünüyor ama sadece "araştırma gerekiyor" olarak işaretlenmiş. Bunun aslında bir hata değil, tasarım sınırlaması olduğundan oldukça eminim. Her özellik duyurulduğu gibi çalışıyor.


Peki ne yapılabilir? Birkaç yaklaşım vardır. Bir yol, arr öğe türünü Foo | Bar öğesinden, ayrık birlik (disjoint union) veya birleşim olmayan (non-union) bir türe değiştirmektir; örneğin bunlardan biri gibi:

type FooNotBar = { x: number, y?: never };
function q(arr: (FooNotBar | Bar)[]) {
    const b = arr.length ? arr[0] : null;
    if (b && typeof b.y !== "undefined") {
        console.log(b.y.toUpperCase()); // okay
    }
}

type FooOrBar = { x: number, y?: string };
function r(arr: (FooOrBar)[]) {
    const b = arr.length ? arr[0] : null;
    if (b && typeof b.y !== "undefined") {
        console.log(b.y.toUpperCase()); // okay
    }
}

Burada, FooNotBar, tanımlı bir y özelliğine sahip olamayacağı için Bar olmayan bir Foo'dur. Bu yüzden FooNotBar | Bar, hangi tür olduğunu belirlemek için y özelliğini kullanabilir. Aynı şekilde, FooOrBar ya bir Bar ya da Bar olmayan bir Foo'dur, çünkü y ya bir string olacaktır ya da tanımlı olmayacaktır. Yine, hangi tür olduğunu belirlemek için y özelliğini kontrol edebilirsiniz. Ancak, bunların hiçbiri artık sadece y anahtarının varlığını kullanarak hangi türe sahip olduğunuzu belirleyemeyeceği için in tip bekçisi ile çalışmaz.


Başka bir yaklaşım, Foo | Bar ifadenizi korumak ve derleyicinin bunu Foo olarak çökmesine izin vermek, ancak varsayılan in tür koruyucu davranışı yerine kendi istediğiniz davranışı vermek için kullanıcı tanımlı bir tür koruma fonksiyonu kullanmaktır:

const isBar = (x: Foo): x is Bar => 'y' in x;

function s(arr: (Foo | Bar)[]) {
    const b = arr.length ? arr[0] : null;
    if (b && isBar(b)) {
        console.log(b.y.toUpperCase()); // okay
    }
}

Burada şunu söylüyorsunuz: “Eğer isBar(foo) çağırırsam, bir true sonucu foo'nun bir Bar gibi muamele görebileceği anlamına gelir”. isBar()'ın uygulaması sadece in kullanıyor, ancak isBar()'ın imzası niyeti iletiyor.


Koda yönelik oyun alanı bağlantısı

İlgili makaleler