Why does type guard work for the object itself, but not for the clones of that object? Here is my code (
Here are the types:
export type Node = {
data: NodeData
id: string;
}
export type NodeData = {
label?: string;
children: NodeDataChildren;
};
type NodeDataChildren =
| {
type: Exclude<NodeDataChildType, NodeDataChildType.CHOICES>;
}
| {
type: NodeDataChildType.CHOICES;
choices?: Array<NodeDataChildChoice>;
};
export enum NodeDataChildType {
CHOICES = "choices",
TEXT = "text",
CONTINUE = "continue",
NONE = "none",
}
export type NodeDataChildChoice = {
id: string;
};
Note: This is not built in Node type, but a custom Node Type
What am I doing wrong here?
CodePudding user response:
I think you're just hitting one of the many limits of automatic type narrowing. I think the primary problem is that your code is narrowing the type of node.data.children
, but then just copying node
. Since node
's type hasn't been narrowed by a type guard, newNode
just gets the plain Node
type — and that type has a union for data.children
. You know (and we know) that node
and newNode
share the same data
object, and thus the same data.children
object, but TypeScript doesn't go that far.
When I run into a limit like that, I like to use a type assertion function, for example:
function assertIsChoicesNodeData(children: NodeDataChildren): asserts children is NodeDataChildrenWithChoices {
if ((children as any).type !== NodeDataChildType.CHOICES) {
throw new Error("Expected a children object with `type` = `NodeDataChildType.CHOICES`");
}
}
Type assertions are a Bad Thing™ most of the time, but a type assertion function is a different beast because it doesn't just trust the programmer, it verifies the assertion at runtime.
Here's how you'd use it:
if (node.data.children.type === NodeDataChildType.CHOICES && node.id === nodeId) {
const newNode = { ...node };
assertIsChoicesNodeData(newNode.data.children);
console.log(node.data.children.choices, newNode.data.children.choices);
}