2

I've come across some answers such as this one, but the wording of my question doesn't return relevant results.

Consider the following:

type TNode = {
    data: {
        id: string;
    } | {
        tree: boolean;
        treeName: string;
    }

}

const nodes: TNode[] = [
    {
        data: {
            id: '1',
        }
    },
    {
        data: {
            id: '2',
        }
    },
    {
        data: {
            id: '3',
            tree: true,
            treeName: 'retrieval',
        }
    }
]

This approach doesn't have TS complain, but it's not what I'm looking for. If I remove the property treeName: 'retrieval' from the last object in nodes, it should complain, saying that I've included tree but I'm missing treeName.

How can I achieve this?

Edit

The accepted answer worked for me, and so did the following approach, which I ended up using because I didn't want to add extra properties to my objects,

interface INodeBase {
    id: string
}

interface INodeWithProps extends INodeBase {
    tree: boolean;
    treeName: string;
}

interface INodeWithOutProps extends INodeBase {
    tree?: never;
    treeName?: never;
}

type TNode = {
    data: INodeWithProps | INodeWithOutProps
}

const nodes: TNode[] = [
    {
        data: {
            id: '1',
        }
    },
    {
        data: {
            id: '2',
        }
    },
    {
        data: {
            id: '3',
            tree: true,
            treeName: 'retrieval',
        }
    },
    {
        data: {
            id: '4',
            tree: 'true', // error, 'treeName' is missing
        }
    },
]
Mike K
  • 7,621
  • 14
  • 60
  • 120

1 Answers1

2

Let's simplify the example a little:

type Node = Leaf | Tree;
type Leaf = {
  id: string
}
type Tree = {
  tree: boolean,
  treeName: string
}

Then, we are allowed to do:

const o = {
  id: '3',
  foo: 43,
  bar: false
}
const n: Node = o; // just fine: o has all properties a Leaf needs

In particular, note that we are generally allowed to provide arbitrary excess properties.

The same happens if excess properties would match another type in the union:

const o = {
  id: '3',
  tree: true,
}
const n: Node = o; // just fine: o has all properties a Leaf needs

TypeScript performs excess property checking only in a few special cases. The most common one is an object literal immediately assigned to an object type:

const o: Tree = {
  id: '3', // error: excess property not declared by Tree
  tree: true,
  treeName: 'retrieval',
}

Given this, one might reasonably expect that excess property checking also applies to union types:

const o: Node = {
  id: '3',
  tree: true,
  treeName: 'retrieval',
}
  

However, that is only the case for discriminated unions, which yours is not.

Therefore, you probably want to change your definition to:

type Leaf = {
  id: string,
  tree: false,
}
type Tree = {
  tree: true,
  treeName: string
}

Then, the value of the tree property tells TypeScript which type it should be type checking against, while enabling excess properties to be rejected in immediately assigned object literals:

const o: Node = {
  id: '3', // error: property id does not exist on Tree
  tree: true,
  treeName: 'retrieval'
}      

const q: Node = {
  tree: true,
  // error: property treeName is missing
}

const x: Node = {
  id: '4',
  tree: false,
  treeName: 'retrieval' // error: property treeName does not exist on Leaf
}
meriton
  • 68,356
  • 14
  • 108
  • 175