Home > front end >  Switching between two different prop types in one React component
Switching between two different prop types in one React component

Time:11-02

TL;DR: Here's the TS playground.

I'm having some trouble with a React component props type definition. I have a component published on npm that currently takes a simple set of props, mostly booleans and strings, with one prop of type RG and some callback functions based around the RG type.

What I want to provide is the same component, but with an optional boolean prop that, when true, "flips" the component to use a new RGIC type instead of RG. Assume this is the current Props interface:

interface Props {
  x: string;
  y: string;
  rg?: RG;
  func(rg: RG): void;
}

I would like to offer an overload of the component with this interface:

interface PropsIC {
  x: string;
  y: string;
  rg?: RGIC;
  func(rg: RGIC): void;
}

It's important that I retain backwards compatibility, so the new prop must be optional in the external API.

These are the types I've come up with:

type RGAny = RG | RGIC;

interface CommonProps {
  x: string;
  y: string;
  flag: boolean; // <-- this is the new prop
}
interface PropsStandard extends CommonProps {
  flag: false;
  query?: RG;
  func(q: RG): void;
}
interface PropsIC extends CommonProps {
  flag: true;
  query?: RGIC;
  func(q: RGIC): void;
}

type Props = (Omit<PropsStandard, 'flag'> & { flag?: boolean }) | (Omit<PropsIC, 'flag'> & { flag?: boolean });

type PropsImpl = PropsStandard | PropsIC;

And this is how I am using them:

NOTE: I'm sure the types above could use some improvements, so feel free to comment on those, but the code block below is where the meat of my question lies.

// This is the exported component, the API surface
export function Component(props: Props) {
  if (props.flag) {
    return ComponentImpl({ ...props, flag: true } as PropsIC)
  }
  return ComponentImpl({ ...props, flag: false } as PropsStandard);
}

// This is the component's internal implementation with two overloads,
// one for RG props (flag is undefined or false) and one for RGIC props (flag === true).
function ComponentImpl(props: PropsStandard): JSX.Element;
function ComponentImpl(props: PropsIC): JSX.Element;
function ComponentImpl({ query, func, flag, x, y }: PropsImpl) {
  // On this next line, I want to declare RG or RGIC instead of RGAny,
  // based on the value of the flag prop (or whether props is PropsStandard or PropsIC).
  const [root, setRoot] = useState<RGAny>(query ?? { combinator: 'and', rules: [] });

  useEffect(() => {
    func(root) // <-- TS error here because RGAny is not assignable to RG
  }, [root])

  return (<div>{flag ? 'True' : 'False'}</div>);
}

This is how the user might implement the component:

// The user's application component
const App = () => {
  const [query, setQuery] = useState<RG>({ combinator: 'and', rules: [] })
  const [queryIC, setQueryIC] = useState<RGIC>({ rules: [] })
  const [flag, setFlag] = useState(false);

  return flag
    ? <Component x="test" y="test" query={queryIC} func={(q: RGIC) => console.log(q)} flag />
    : <Component x="test" y="test" query={query} func={(q: RG) => console.log(q)} />;
}

ReactDOM.render(<App />, document.body);

I understand how to use custom type guards and the in operator (like here and here), but I'm not sure how to apply them to the entire component function body without sort of duplicating the entire implementation.

Update: this TS playground has updated typings based on @chazsolo's comment.

CodePudding user response:

Instead of adding an optional flag to your component, consider adding a generic type similar to (not complete code):

interface Props<T> & CommonProps {
  func(q: T): void;
  query: T;
}

function Component<T extends RG | RGIC = RG>(props: Props<T>) {
   // ...
}

T extends RG | RGIC = RG allows you to set the valid types that can be used (RG or RGIC) and defaults to RG in case a user doesn't pass one in. The correct type will then be inferred based on what was given to the component (so things like the parameters in func don't need to be re-typed when used).

Usage will look like:

return flag
  ? <Component<RGIC> query={queryIC} func={(q) => console.log(q)} />
  : <Component<RG> query={query} func={(q) => console.log(q)} />;
  • Related