Search code examples
typescriptreact-typescript

How to preserve type information based on object key in TypeScript?


I have items of varying types (Item) and these can exist at different levels in my state (Section.items and subSection.items).

I need to pass these to a different React component based on their type (Str or Num).

The following code works however it’s not ideal data structuring.

type I_Str = {
  type: "str";
  value: string;
};
type I_Num = {
  type: "num";
  value: number;
};
type Item = I_Str | I_Num;

type SubSection = {
  name: string;
  items: Item[];
};

type Section = {
  name: string;
  items: Item[];
  subSection?: SubSection[];
};

type State = {
  section: Section[];
};

const state: State = {
  section: [
    {
      name: "Sec1",
      items: [
        { type: "str", value: "im a string" },
        { type: "num", value: 1 },
      ],
      subSection: [
        {
          name: "Sub1",
          items: [
            { type: "num", value: 999 },
            { type: "str", value: "sub section string" },
          ],
        },
      ],
    },
  ],
};

function Str({ item }: { item: I_Str }) {
  return <div>Value: {item.value.toUpperCase()}</div>;
}
function Num({ item }: { item: I_Num }) {
  return <div>Value: {item.value * 10}</div>;
}

function Items({ items }: { items: Item[] }) {
  return (
    <div style={{ border: "1px solid blue", padding: 20 }}>
      {items
        .sort((a, b) => {
          if (a.type > b.type) return 1;
          if (a.type < b.type) return -1;
          return 0;
        })
        .map((item) => {
          if (item.type === "str") return <Str item={item} />;
          if (item.type === "num") return <Num item={item} />;
        })}
    </div>
  );
}

function SubSection({ subSection }: { subSection: SubSection }) {
  return (
    <div>
      <div>{subSection.name}</div>
      <Items items={subSection.items} />
    </div>
  );
}

function Section({ section }: { section: Section }) {
  return (
    <div key={section.name} style={{ border: "1px solid grey", padding: 20 }}>
      <div>{section.name}</div>
      <Items items={section.items} />
      {section.subSection && section.subSection.length > 0
        ? section.subSection.map((sub) => {
            return <SubSection key={sub.name} subSection={sub} />;
          })
        : null}
    </div>
  );
}

export default function Page() {
  return (
    <div>
      {state.section.map((section) => {
        return <Section key={section.name} section={section} />;
      })}
    </div>
  );
}

It’s not ideal as items is an array but the order isn’t important.

Also you should only be able to have one item of each type eg this shouldn't be allowed:

items: [
  { type: "str", value: “String 1” },
  { type: "str", value: “String 2” },
]

I can change Items to be an object and then im happy with the data structure, but TypeScript errors:

type Items = {
  str?: string;
  num?: number;
};

type SubSection = {
  name: string;
  items: Items;
};

type Section = {
  name: string;
  items: Items;
  subSection?: SubSection[];
};

type State = {
  section: Section[];
};

const state: State = {
  section: [
    {
      name: "Sec1",
      items: {
        str: "Im a st",
        num: 1,
      },
      subSection: [
        {
          name: "Sub1",
          items: {
            num: 999,
            str: "sub section string",
          },
        },
      ],
    },
  ],
};

function Str({ value }: { value: string }) {
  return <div>Value: {value.toUpperCase()}</div>;
}
function Num({ value }: { value: number }) {
  return <div>Value: {value * 10}</div>;
}

function Items({ items }: { items: Items }) {
  return (
    <div style={{ border: "1px solid blue", padding: 20 }}>
      {["num", "str"].map((key) => {
        const value = items[key];
        if (!value) return;
        if (key === "num") return <Num value={value} />;
        if (key === "str") return <Str value={value} />;
      })}
    </div>
  );
}

function SubSection({ subSection }: { subSection: SubSection }) {
  return (
    <div>
      <div>{subSection.name}</div>
      <Items items={subSection.items} />
    </div>
  );
}

function Section({ section }: { section: Section }) {
  return (
    <div key={section.name} style={{ border: "1px solid grey", padding: 20 }}>
      <div>{section.name}</div>
      <Items items={section.items} />
      {section.subSection && section.subSection.length > 0
        ? section.subSection.map((sub) => {
            return <SubSection key={sub.name} subSection={sub} />;
          })
        : null}
    </div>
  );
}

export default function Page() {
  return (
    <div>
      {state.section.map((section) => {
        return <Section key={section.name} section={section} />;
      })}
    </div>
  );
}

const value = items[key]; // Error occurs here

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Items'.   No index signature with a parameter of type 'string' was found on type 'Items'.

What is the best way to model this? Im simplified my example code, in reality I have many more types than just str and num and their values can also be more complex (eg varying objects not just primitive types).


Solution

  • The first thing I did is rename the component name (to ItemsComp) because you had used the same names for both type and component. eg:- type Items and function Items()

    My question is do you have a dynamic count of types? I guess not, because you have created separate components for each type. eg:- function Str(), function Num()

    If I am correct can you do something like that?

    function ItemsComp({ items }: { items: Items }) {
      return (
        <div style={{ border: "1px solid blue", padding: 20 }}>
          {items.str && <Str value={items.str} />}
          {items.num && <Num value={items.num} />}
          {/* other types */}
        </div>
      );
    }
    

    And I am not getting any errors for this one

    function ItemsComp({ items }: { items: Items }) {
      return (
        <div style={{ border: "1px solid blue", padding: 20 }}>
          {(["num", "str"] as const).map((key) => {
            const value = items[key];
            if (!value) return null;
            if (key === "num") return <Num key={key} value={Number(value)} />;
            if (key === "str") return <Str key={key} value={`${value}`} />;
            return null;
          })}
        </div>
      );
    }