Max Green
Max Green

Reputation: 449

React + TS: How to use dot-notation components and generics at the same time?

I have a component List that has a sub-component Title, declared using dot-notation.

type Item = {
  name: string;
  value: number;
};

type ListProps = {
  title: string;
  data: Item[];
};

export const List: React.FC<ListProps> & {
  Title: React.FC<{ title: string }>
} = ({ title, data }) => {
  return (
    <div>
      <List.Title title={title}>
      <ul>
        {data.map((item) => (
          <li>
            {item.name}: {item.value}
          </li>
        ))}
      </ul>
    </div>
  );
};

List.Title = ({title}) => <div>{title}</div>

And then i use this component:

<List
    title="Stats"
    data={[
        { name: "strength", value: 30 },
        { name: "dexterity", value: 50 },
        { name: "intellect", value: 100 },
        { name: "vitality", value: 40 }
    ]}
/>

At this point everything is fine, but if i need to add generic type parameter to component and its interface it seems like there is no way to do this.

For example i want item name to be of certain type, that i can pass from parent.

<List<"strength" | "dexterity" | "intellect" | "vitality">
    title="Stats"
    data={[
        { name: "strength", value: 30 },
        { name: "dexterity", value: 50 },
        { name: "intellect", value: 100 },
        { name: "vitality", value: 40 }
    ]}
/>

I know i can do it like this

type Item<ItemName extends string = string> = {
  name: ItemName;
  value: number;
};

type ListProps<ItemName extends string = string> = {
  title: string;
  data: Item<ItemName>[];
};

export const List = <ItemName extends string = string,>({
  title,
  data
}: ListProps<ItemName>): JSX.Element => {
  return (
    <div>
      <ul>
        {data.map((item) => (
          <li>
            {item.name}: {item.value}
          </li>
        ))}
      </ul>
    </div>
  );
};

// This isn't working, because `List` has no property `Title`
// List.Title = ({ title }) => <div>{title}</div>;

But in this case i don't know how i can specify type for List.Title. Is there any way to combine this two techniques?

Codesandbox

Upvotes: 1

Views: 408

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075915

This isn't working, because List has no property Title

That's not the error TypeScript is raising in the Codesandbox (or in the playground). The error is "Binding element 'title' implicitly has an 'any' type. (7031)" That's because you haven't given any type for the props of List.Title. The Title props used to get a type from the type you'd given List, but since you've removed the explicit type annotation on List, you need to specify the type of Title's title somewhere.

The simple change is to just specify it inline:

List.Title = ({ title }: {title: string; }) => <div>{title}</div>;
//                     ^^^^^^^^^^^^^^^^^^^

Playground | Codesandbox

Alternatively, you can do what you did in your first example and give List an explicit type. The type is:

{
    <ItemName extends string = string>({ title, data }: ListProps<ItemName>): JSX.Element;
    Title({ title }: {
        title: string;
    }): JSX.Element;
}

so:

export const List: {
    <ItemName extends string = string>({ title, data }: ListProps<ItemName>): JSX.Element;
    Title({ title }: {
        title: string;
    }): JSX.Element;
} = <ItemName extends string = string,>({/*...*/}) => {
    // ...
};
List.Title = ({ title }) => <div>{title}</div>;

Playground

Upvotes: 1

Related Questions