Hvorfor antar ikke Typescript typen i switch-setninger over dypt/nestede egenskaper


Hvorfor fungerer typeinferenz i eksempel A, men ikke i B? Den eneste forskjellen er plasseringen av typestrengen. block.type vs block.meta.type. A kompilerer og infererer type, mens B resulterer i.

// Example B errors
Property 'a' does not exist on type 'Block'. Property 'a' does not exist on type 'ITwo'.
Property 'b' does not exist on type 'Block'. Property 'b' does not exist on type 'IOne'.

Hvordan får jeg B til å kompilere og inferere korrekt, uten å endre datastrukturen til IOne eller ITwo?


Eksempel A

https://www.typescriptlang.org/play?#code/KYOwrgtgBAKgmgBQKJQN5QPIDkUF4oDkA9iMAQDSwDqGU+BALgO5EEC+AUBwJYgPAAnAGYBDAMbAoASQyk0HKIqgiAXFADODAbwDm5BUoYBPAA7A18ZADpsSDpx59BoidJgt5SqACM1m7SB6BorGZhaISFYwNPZcoZIAQgA2RGIA1nTSspIAPm4sXADaALpWECImABSV3inpKsmpaQCUdAB8nkrqTNwMYgAWUDV1aVbxrajBXlBiIuqSlpG2KlPTSrVNViKraz4CwCJpANw7irPzsBFRNCu70xvpVt5Qp-f7hydenGzNR0A

enum TYPE { ONE = 'one', TWO = 'two'}

interface IOne {
    a: string,
    type: TYPE.ONE
}

interface ITwo {
    b: string,
    type: TYPE.TWO
}

type Block = IOne | ITwo

[].map((block:Block) => {
    switch (block.type) {
        case TYPE.ONE:
            block.a
            break;
        case TYPE.TWO:
            block.b 
            break;
    }
});

Eksempel B

https://www.typescriptlang.org/play?#code/KYOwrgtgBAKgmgBQKJQN5QPIDkUF4oDkA9iMAQDSwDqGU+BALgO5EEC+AUBwJYgPAAnAGYBDAMbAoASQyk0HKIqgiAXFADODAbwDm5BUojAGIqGtQGlShgE8ADsDXxkAOmxJLUTpx59BoiWkYFnkrACM1TW0QPU8jEzNQq2t7R1hEJBcYGk9vLlsHKAAhABsiMQBrOmlZSQAfIJYuAG0AXRcIETsACm6wssqVUvKKgEo6AD4kxXUmbgYxAAsoPoGKjuMRFwLgcYtkpTERdUlnTPcVTwPFfpGXESvrsIFgEQqAbkfFI5P012yMJdrslbpUXGEoF8QS83p8rN5Ru8gA

enum TYPE { ONE = 'one', TWO = 'two'}

interface IOne {
    a: string,
    meta : {
        type: TYPE.ONE
    }
}

interface ITwo {
    b: string,
    meta : {
        type: TYPE.TWO
    }
}

type Block = IOne | ITwo

[].map((block:Block) => {
    switch (block.meta.type) {
        case TYPE.ONE:
            block.a
            break;
        case TYPE.TWO:
            block.b 
            break;
    }
});

Takk på forhånd, J.

Svar #1

Dette blir diskutert og fulgt opp under saken Nested Tagged Unions i TS-repoet. Kort svar på spørsmålet ditt: du vil ikke kunne gjøre det du ønsket før saken er løst.

Typebeskyttere og generiske typer

Når det er sagt, kan du fortsatt oppnå dette med en kombinasjon av typebeskyttere og generiske typer. Typebeskytteren vil utføre en kjøretidskontroll for å sikre at en nestet verdi samsvarer med forventet type, og generiske typer vil fjerne noe av standardkoden som ellers er nødvendig for å sjekke hver type i unionen.

Siden eksemplet vi diskuterer er ganske abstrakt, kan dette være en lite praktisk løsning for din kode i den virkelige verden.

Forutsatt at det bare er et håndfull typer, gir det mening å kvitte seg med switch-setningen til fordel for betingede retursetninger ("Returner tidlig, returner ofte"). Dette vil gjøre bruk av en typebeskyttelse trivielt:

enum TYPE { ONE = 'one', TWO = 'two'}

interface IOne {
    a: string,
    meta : {
        type: TYPE.ONE
    }
}

interface ITwo {
    b: string,
    meta : {
        type: TYPE.TWO
    }
}

type Block = IOne | ITwo;

export const isBlock = <T extends Block>(
  b: Block,
  metaType: TYPE,
): b is T =>
  b.meta.type === metaType;

[].map((block: Block) => {
    if (isBlock<IOne>(block, TYPE.ONE)) {
        return block.a;
    }
    
    return block.b; 
});

Relaterte artikler