Conditional TypeScript Generics in React Components

Published: (January 9, 2026 at 09:01 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

In my previous article here I promised to continue the discussion of TypeScript generics – a topic that I consider especially important when working with React components.

In that article we covered the basic definitions of generic components and looked at a simple example of when and why generics can be useful. In this post we will take the next step and focus on conditional generics and how they can be applied in real‑world React components.

Here I would like to look at an example from a real project I worked on, which showed me just how elegant and powerful generic types can be.

The problem: a selection component without generics

Let’s consider a selection component that allows us to select one or a few options depending on props.

// Just an interface of option structure
interface Option {
  id: number;
  name: string;
}

// It's your value type
type Value = Option[] | Option | undefined;

// Component props interface
interface Props {
  value?: Value;
  options?: Option[];
  multiple?: boolean;
  onChange?: (value: Value) => void;
}

When using this component you are not fully safe with the value type, because TypeScript doesn’t know whether the component is used in multiple‑selection mode or not.
value?: Value and onChange?: (value: Value) => void are always available in both states, independent of multiple?: boolean. This is not strict enough – we would like a guarantee that:

  • if multiple is false (or omitted) → value must be a single object (or undefined);
  • if multiple is truevalue must be an array.

Introducing generics to the Props interface

We keep the same Option interface:

interface Option {
  id: number;
  name: string;
}

Now we enhance Props with two generic parameters, O and M:

interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
  value?: M extends undefined | false ? O | undefined : O[];
  options?: O[];
  multiple?: M;
  onChange?: (
    values: M extends undefined | false ? O | undefined : O[]
  ) => void;
}

Explanation

GenericMeaning
OExtends Option; represents the option type.
MExtends `boolean
valueM extends undefined | false ? O | undefined : O[] – a single O (or undefined) when multiple is false/omitted, otherwise an array O[].
onChangeSame conditional type for the callback argument.

Extracting the conditional type

Repeating the conditional expression is noisy, so we extract it into its own generic type:

type Value<O extends Option, M extends boolean | undefined> =
  M extends undefined | false ? O | undefined : O[];

Now Props becomes much cleaner:

interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
  value?: Value<O, M>;
  options?: O[];
  multiple?: M;
  onChange?: (values: Value<O, M>) => void;
}

Adding generics to the component itself

The component must also be generic and pass those parameters to Props:

export const SelectionExample = <
  O extends Option = Option,
  M extends boolean | undefined = undefined
>({
  value = undefined,
  multiple = false,
  onChange = () => undefined,
  options = [],
}: Props<O, M>) => {
  const isOptionSelected = (option: O): boolean => {
    // Implementation using `value` and `multiple`
    return false;
  };

  const handleOptionClick = (option: O) => {
    if (multiple) {
      // Implementation for multiple selection using `onChange`
    } else {
      // Implementation for single selection using `onChange`
    }
  };

  return (
    <div>
      {options.map((option) => {
        const selected = isOptionSelected(option);
        return (
          <div key={option.id}>
            {/* Render option UI */}
          </div>
        );
      })}
    </div>
  );
};

Note: The generic parameters <O, M> are declared on the component and then supplied to Props.

Using the generic component

1️⃣ Multiple selection

export const UsageExample = () => {
  const options: Option[] = [
    { id: 1, name: "First" },
    { id: 2, name: "Second" },
    { id: 3, name: "Third" },
  ];

  return (
    <SelectionExample
      multiple
      options={options}
      onChange={(selected) => {
        // `selected` is inferred as `Option[]`
        console.log("selected:", selected);
      }}
    />
  );
};

2️⃣ Single selection

export const SingleUsage = () => {
  const options: Option[] = [
    { id: 1, name: "First" },
    { id: 2, name: "Second" },
    { id: 3, name: "Third" },
  ];

  return (
    <SelectionExample
      options={options}
      onChange={(selected) => {
        // `selected` is inferred as `Option | undefined`
        console.log("selected:", selected);
      }}
    />
  );
};

In both cases TypeScript correctly infers the shape of value and the argument type of onChange, giving us the strict guarantees we wanted.

Takeaways

  • Conditional generics let us express relationships between props (e.g., multiplevalue shape).
  • Extracting repeated conditional logic into a reusable generic type (Value) keeps the code DRY and readable.
  • Declaring generics on the component itself and forwarding them to the props interface ensures the whole component is type‑safe.

With this pattern you can build highly reusable, type‑safe React components that adapt their API based on the values of their props. Happy typing!

Multiple selection (additional example)

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `values` is the selected options array */}
      {console.log(values)}
    </>
  );
};

Single selection (additional example)

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `value` is the selected option */}
      {console.log(value)}
    </>
  );
};

Wrong usage

export const UsageExample = () => {
  return (
    <>
      {/* ... */}
      {/* `values` has type `Option | undefined` – this is a misuse when multiple is expected */}
      {console.log(values)}
    </>
  );
};

I know this article may have pushed you to go deeper into TypeScript features. However, I believe you’ll feel satisfied once you finish your own similar implementation and realize how much more powerful your component has become.

Back to Blog

Related posts

Read more »