skip to Main Content

I have an item of state that can either have it’s own value, or reference another item via an id.

interface Item {
  name: string;
  value: { valueLiteral: number } | { id: string };
}
const item1: Item = {
  name: "foo1",
  value: {
    valueLiteral: 1
  }
};
const item2: Item = {
  name: "foo2",
  value: {
    id: "some-id"
  }
};

I need to have an array of these items and pass them to a React component, and have the component render differently depending on whether value has a valueLiteral or id property.

This code works but gets a TypeScript error:
https://codesandbox.io/s/strange-wilbur-i4bvjm?file=/src/App.tsx

interface Item {
  name: string;
  value: { valueLiteral: number } | { id: string };
}
const item1: Item = {
  name: "foo1",
  value: {
    valueLiteral: 1
  }
};
const item2: Item = {
  name: "foo2",
  value: {
    id: "some-id"
  }
};
interface State {
  items: Item[];
}
const state: State = {
  items: [item1, item2]
};
function Item({ item }: { item: Item }) {
  if (item.value.id) {
    return (
      <div>
        <div>
          {item.name} - ID = {item.value.id}
        </div>
      </div>
    );
  }
  return (
    <div>
      <div>
        {item.name} - VALUE = {item.value.valueLiteral}
      </div>
    </div>
  );
}
export default function App() {
  return (
    <div className="App">
      {state.items.map((item) => {
        return <Item key={item.name} item={item} />;
      })}
    </div>
  );
}

Property ‘id’ does not exist on type ‘{ valueLiteral: number; } | { id: string; }’.
Property ‘id’ does not exist on type ‘{ valueLiteral: number; }’.ts(2339)

How can I fix this error given my data modelling requirements?

I could have separate ID and value fields and make them both optional. This avoids the TypeScript errors but is less type safe as TypeScript doesn’t prevent Item from having both or neither value and id, but this is not valid for my data modelling.

https://codesandbox.io/s/fervent-euler-h65899?file=/src/App.tsx

interface Item {
  name: string;
  value?: number;
  id?: string;
}
const item1: Item = {
  name: "foo1",
  value: 1
};
const item2: Item = {
  name: "foo2",
  id: "some-id"
};
// item3 is invalid
const item3: Item = {
  name: "foo3",
};
// item4 is invalid
const item4: Item = {
  name: "foo3",
  id: 'some-other-id',
  value: 2
};
interface State {
  items: Item[];
}
const state: State = {
  items: [item1, item2]
};
function Item({ item }: { item: Item }) {
  if (item.id) {
    return (
      <div>
        <div>
          {item.name} - ID = {item.id}
        </div>
      </div>
    );
  }
  return (
    <div>
      <div>
        {item.name} - VALUE = {item.value}
      </div>
    </div>
  );
}
export default function App() {
  return (
    <div className="App">
      {state.items.map((item) => {
        return <Item key={item.name} item={item} />;
      })}
    </div>
  );
}

3

Answers


  1. Create a type or interface for the value:

    interface ValueLiteral {
      valueLiteral: number;
    }
    
    interface ValueId {
      id: string;
    }
    
    interface Item {
      name: string;
      value: ValueLiteral | ValueId;
    }
    
    

    And then check the type:

    
    function isValueLiteral(value: ValueLiteral | ValueId): value is ValueLiteral {
      return "valueLiteral" in value;
    }
    
    function Item({ item }: { item: Item }) {
      if (isValueLiteral(item.value)) {
        return (
          <div>
            <div>
              {item.name} - VALUE = {item.value.valueLiteral}
            </div>
          </div>
        );
      } else {
        return (
          <div>
            <div>
              {item.name} - ID = {item.value.id}
            </div>
          </div>
        );
      }
    }
    
    Login or Signup to reply.
  2. To fix this error while still maintaining the type safety, you can use type guards. I believe this would fix your problem:

    function isValueWithId(value: any): value is { id: string } {
      return value && typeof value === 'object' && 'id' in value;
    }
    ...
    ...
    
      if (isValueWithId(item.value)) {
        return (
          <div>
            <div>
              {item.name} - ID = {item.value.id}
            </div>
          </div>
        );
      }
    else{
      return (
        <div>
          <div>
            {item.name} - VALUE = {item.value.valueLiteral}
          </div>
        </div>
      );
    }}
    
    Login or Signup to reply.
  3. if (item.value.id) {...} means if item.value.id is truthy, but { valueLiteral: number } has no id so how can I check that? That’s what TS complains about.

    TS doesn’t get what you’re implying with that check.

    But you can use the in operator for that.

    if ("id" in item.value) {...} means if item.value is an object that has an id property. Here TS can understand that you’re differentiating between an object with and one without an id property.

    function Item({ item }: { item: Item }) {
      if ("id" in item.value) {
        return (
          <div>
            <div>
              {item.name} - ID = {item.value.id}
            </div>
          </div>
        );
      }
      return (
        <div>
          <div>
            {item.name} - VALUE = {item.value.valueLiteral}
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search