reactjstypescripttypescript-genericsreact-typescript

Creating a generic Context that allows for the children within to be inferred correctly


I'm having some trouble to wrap my head around how to create a generic context. Basically what i want is to be able to pass a value to the Root, and then have the children infer the correct type, is this even possible? I know that it's more than likely because there's no connection between TabHeaderProps, TabContextProps and TabRootProps.

This is a simplified version that works, but it's more verbose than I want it to be.

import React, { Dispatch, SetStateAction, useState } from 'react';

type Headers = 'Foo' | 'Bar' | 'Baz';
const defaultValue: Headers = 'Foo';

type TabsContextProps<T = any> = {
  activeTab: T;
  setActiveTab: Dispatch<SetStateAction<T | undefined>>;
};

const TabsContext = React.createContext<TabsContextProps | undefined>(
  undefined,
);

type TabsRootProps<T> = {
  defaultValue: T;
  children: React.ReactNode;
};

export function TabsRoot<T>({ children }: TabsRootProps<T>) {
  const [activeTab, setActiveTab] = useState<T>();

  return (
    <TabsContext.Provider
      value={{
        setActiveTab,
        activeTab,
      }}
    >
      {children}
    </TabsContext.Provider>
  );
}

function useTabsRootContext<T>(): TabsContextProps<T> {
  if (!React.useContext(TabsContext)) {
    throw new Error('useTabsRootContext must be used inside TabsRoot');
  }
  return React.useContext(TabsContext) as TabsContextProps<T>;
}

type TabHeaderProps<T> = {
  value: T;
};

const TabsHeader = <T,>({ value }: TabHeaderProps<T>) => {
  const { activeTab } = useTabsRootContext<T>();
  if (!activeTab !== value) {
    return null;
  }
  return <p>Im Active</p>;
};

const Tabs = () => {
  <TabsRoot<Headers> defaultValue={defaultValue}>
    <TabsHeader<Headers> value="Baz" /> {/* This work but i preferably want this to be infered based on the TabsRootType */}
  </TabsRoot>;
};

Ideally i would want to have it:

const Tabs = () => {
  <TabsRoot defaultValue={defaultValue}>
    <TabsHeader value="Baz" />
    <TabsHeader value="error" /> {/* I want this to throw an error */}
  </TabsRoot>;
};

Solution

  • Unfortunately this is indeed impossible: