Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Type guards not detecting cloned objects

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Property 'choices' does not exist on type 'NodeDataChildren'.
  Property 'choices' does not exist on type '{ type: NodeDataChildType.TEXT | NodeDataChildType.CONTINUE | NodeDataChildType.NONE; }'.ts(2339)

enter image description here
enter image description here

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

Playground link

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading