Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't Typescript Infer Type in Switch Statements over Deep/Nested Properties

Tags:

typescript

Why does type inference work in example A but not in B? The only difference is the position of the type string. block.type vs block.meta.type. A compiles, and infers type and B results in.

// 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'.

How do I get B to compile, and infer correctly, without changing the data structure of IOne or ITwo?


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

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

Thanks in advance, J.

like image 614
u840903 Avatar asked Dec 04 '20 07:12

u840903


1 Answers

This is discussed and tracked under the Nested Tagged Unions issue in the TS repo. Short answer to your question: you won't be able to do what you wanted until the issue is fixed.

Type guards & generics

That said, you can still achieve this with a combination of type guards and generics. The type guard will perform a runtime check that a nested value matches the expected type, and generics will remove some boilerplate otherwise necessary to check for every type in the union.

As the example we are discussing is pretty abstract, this may not be a practical solution for your real world code.

Assuming there's only a handful of types, it makes sense to get rid of the switch statement in favor of conditional returns ("Return early, return often"). This would make using a type guard trivial:

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; 
});
like image 79
Nikolay Shebanov Avatar answered Oct 19 '22 20:10

Nikolay Shebanov