Zabráňte zužovaniu typu TypeScript v podmienenom bloku


Zdá sa, že TypeScript zužuje zjednotenie pri analýze riadenia toku:

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
}

Predpokladám, že typ a je Foo | null, pretože Foo je podtyp Bar a teda je to minimálny bezpečný typ tu. Ako môžem zabrániť tomu, aby k tomuto zužovaniu došlo? Napríklad, čo ak by som chcel urobiť nasledovné:

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);
    }
}

Odpoveď č. 1

Keďže každý Bar je tiež Foo, množina Foo | Bar je ekvivalentná len Foo, pokiaľ ide o hodnoty, ktoré im môžu byť priradené. V istom zmysle Foo | Bar neobsahuje viac informácií než len Foo, podobne ako string | "hello" obsahuje rovnaké informácie ako len string.

Keď kompilátor explicitne prevedie Foo | Bar na Foo, nazýva sa to „redukcia podtypu“ (ako je implementované v microsoft/TypeScript#4537). To je správne, ale niekedy otravné, pretože hoci technicky | Bar neposkytuje žiadne ďalšie informácie o tom, aké typy hodnôt tam môžu byť, pragmaticky to naznačuje, že vlastnosť y možno možno použiť na určenie, čo to je.


Operátor in sa považuje za strážca typu (ako je implementované v microsoft/TypeScript#15256), kde sa prítomnosť vlastnosti používa na zúženie zjednoteného typu. Toto je užitočné, ale nespoľahlivé. Pragmaticky povedané, očakávate, že ak {a: 0} | {b: 1} obsahuje vlastnosť a, potom ide o {a: 0}, ale technicky vzaté, nemôžete to predpokladať. Nakoniec, hodnota {a: "oops", b: 1} je platná pre {a: 0} | {b: 1}, ale nie je platná pre {a: 0}. Sama prítomnosť vlastnosti a vám technicky nedáva dostatok informácií na zúženie typu, ale pragmaticky áno.


Tieto dva spôsoby zaobchádzania s vlastnosťami a zjednoteniami sú v rozpore a v prípade ako ten váš navzájom zle interagujú.

Najprv sa Foo | Bar zredukuje na Foo (technicky správne), ktorý nemá známy atribút y. Potom používate operátor in na kontrolu atribútu y, čím sa rozhodne, že to nemôže byť Foo vôbec kvôli nedostatku známeho atribútu y (technicky nesprávne).

Existuje problém microsoft/TypeScript#37518, ktorý sa zdá byť presne tento, ale je označený len ako „potrebuje preskúmanie“. Som si celkom istý, že ide len o obmedzenie dizajnu a nie o chybu ako takú. Každá funkcia funguje tak, ako je deklarované.


Tak čo sa dá robiť? Existuje niekoľko prístupov. Jedným spôsobom je zmeniť typ prvku arr z Foo | Bar na buď disjunktívny zväzok, alebo na typ, ktorý nie je zväzkom, napríklad jeden z týchto:

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
    }
}

Tu je FooNotBar Foo, ktorý nie je Bar, pretože nemôže mať definovanú vlastnosť y. Takže FooNotBar | Bar môže použiť vlastnosť y na zistenie, o ktorý typ ide. Rovnakým spôsobom je FooOrBar Foo, ktorý je buď Bar alebo nie Bar, pretože y bude buď string, alebo nebude definovaný. Opäť môžete skontrolovať vlastnosť y, aby ste zistili ktorý. Všimnite si však, že žiadny z týchto typov nefunguje s typovou ochranou in, pretože už nemožno použiť len prítomnosť kľúča y na zistenie, o ktorý typ ide.


Ďalším prístupom je ponechať svoj Foo | Bar a nechať kompilátor zložiť ho na Foo, ale použiť používateľom definovanú funkciu ochrany typu (type guard), aby ste dosiahli vlastné požadované správanie namiesto predvoleného správania ochrany typu in:

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
    }
}

Tu hovoríte „ak zavolám isBar(foo), výsledok true znamená, že foo možno považovať za Bar“. Implementácia isBar() používa iba in, ale signatúra isBar() komunikuje zámer.


Odkaz na kód na ihrisku

Súvisiace články