Why does type guard work for the object itself, but not for the clones of that object? Here is my code (playground link):
if (
node.data.children.type === NodeDataChildType.CHOICES &&
node.id === nodeId
) {
const newNode = { ...node };
console.log(
newNode.data.children.choices, // cloned object, giving error
node.data.children.choices // original object, no errors
);
}
(Please note: I know both choices above are the same array since object spread just does a shallow clone. I’m logging both of them to demonstrate the type error on the cloned object.)
I’m getting this error:
Property 'choices' does not exist on type 'NodeDataChildren'.
Property 'choices' does not exist on type '{ type: NodeDataChildType.TEXT | NodeDataChildType.CONTINUE | NodeDataChildType.NONE; }'.ts(2339)
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?
>Solution :
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);
}

