2

Here is a simplified version of a Tabs component that exposes a method called activateTab through useImperativeHandle:

type TabsProps<TabName> = {
  tabs: readonly { name: TabName }[];
  children: ReactNode;
};

const TabsComponent = <TabName extends string>(
  props: TabsProps<TabName>,
  ref: Ref<{ activateTab: (tabName: TabName) => void }>
) => {
  const { tabs, children } = props;

  useImperativeHandle(ref, () => ({
    activateTab: (tabName: TabName) => {}
  }));

  return (
    <div>
      <div role="tablist">
        {tabs.map(({ name }) => (
          <button type="button" role="tab" key={name}>
            {name}
          </button>
        ))}
      </div>
      {children}
    </div>
  );
};

const Tabs = forwardRef(TabsComponent);

// Usage:

const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

export default function App() {
  return (
    <Tabs tabs={tabs}>
      {...}
    </Tabs>
  );
}

All is good until this point. Here is a working CodeSandbox.

But, now I want to add a Tabs.Panel component so the usage is:

export default function App() {
  return (
    <Tabs tabs={tabs}>
      <Tabs.Panel>Content</Tabs.Panel>
    </Tabs>
  );
}

I tried the following, but TypeScript complains:

type PanelProps = {
  children: ReactNode;
};

const Panel = ({ children }: PanelProps) => {
  return <div role="tabpanel">{children}</div>;
};

Tabs.Panel = Panel;
     ~~~~~
       ^
       Property 'Panel' does not exist on type 'ForwardRefExoticComponent<TabsProps<string> & RefAttributes<{ activateTab: (tabName: string) => void; }>>'

What's the best way to achieve this Tabs.Panel API in TypeScript?

Non-working CodeSandbox

Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746

1 Answers1

2

You need to use Object.assign:

import React, { forwardRef, ReactNode, Ref, useImperativeHandle, FC } from "react";

type PanelProps = {
  children: ReactNode;
};

const Panel = ({ children }: PanelProps) => {
  return <div role="tabpanel">{children}</div>;
};

type TabsProps<TabName> = {
  tabs: readonly { name: TabName }[];
  children: ReactNode;
};

const TabsComponent = <TabName extends string>(
  props: TabsProps<TabName>,
  ref: Ref<{ activateTab: (tabName: TabName) => void }>
) => {
  const { tabs, children } = props;

  useImperativeHandle(ref, () => ({
    activateTab: (tabName: TabName) => { }
  }));

  return (
    <div>
      <div role="tablist">
        {tabs.map(({ name }) => (
          <button type="button" role="tab" key={name}>
            {name}
          </button>
        ))}
      </div>
      {children}
    </div>
  );
};

const Tabs = Object.assign(forwardRef(TabsComponent), { Panel });

const tabs = [{ name: "Tab 1" }, { name: "Tab 2" }] as const;

export default function App() {
  return <Tabs tabs={tabs}>
    <Tabs.Panel>Content</Tabs.Panel> // ok
  </Tabs>;
}

Playground

Here you can find more explanation about using static properties on functions