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.
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.
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;
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With