Generic type React component in TypeScript

Thomas Rubattel
3 min readMar 12, 2023
Riemannian geometry generalises Euclidean geometry. Credits: Milad Fakurian on Unsplash

In the present article, we’ll discuss on how to write a Generic type React component. Generics are the hardest TypeScript features to get into. We’ll also tackle conditional type, utility type and never type.

Specification

The generic type component we will implement is a drop-down list.

import { ChangeEvent, useState } from "react";
import Select from "./Select";

function App() {
const [state, setState] = useState({ language: "" });

const onChange = (e: ChangeEvent<HTMLSelectElement>) =>
setState((prev) => ({ ...prev, [e.target.name]: e.target.value }));

return (
// ✅ options as Array<string>. No renderOption props can be passed.
<Select
name="language"
label="Preferred language"
onChange={onChange}
value={state.language}
options={["German", "Polish", "Swedish", "Japanese"]}
/>
);
}

export default App;
import { ChangeEvent, useState } from "react";
import Select from "./Select";

function App() {
const [state, setState] = useState({ language: "" });

const onChange = (e: ChangeEvent<HTMLSelectElement>) =>
setState((prev) => ({ ...prev, [e.target.name]: e.target.value }));

return (
// 💥 options as Array<object>. renderOption props has to be passed.
<Select
name="language"
label="Preferred language"
onChange={onChange}
value={state.language}
options={[
{ iso: "de", plainText: "German" },
{ iso: "pl", plainText: "Polish" },
{ iso: "sv", plainText: "Swedish" },
{ iso: "ja", plainText: "Japanese" },
]}
/>
);
}

export default App;
import { ChangeEvent, useState } from "react";
import Select from "./Select";

function App() {
const [state, setState] = useState({ language: "" });

const onChange = (e: ChangeEvent<HTMLSelectElement>) =>
setState((prev) => ({ ...prev, [e.target.name]: e.target.value }));

return (
// ✅ options as Array<object>. renderOption props has to be passed.
<Select
name="language"
label="Preferred language"
onChange={onChange}
value={state.language}
options={[
{ iso: "de", plainText: "German" },
{ iso: "pl", plainText: "Polish" },
{ iso: "sv", plainText: "Swedish" },
{ iso: "ja", plainText: "Japanese" },
]}
renderOption={(opt) => (
<option value={opt.iso}>
{opt.plainText}
</option>
)}
/>
);
}

export default App;

Implementation

// Select.tsx
import { Fragment } from "react";

type RenderOption<T> = T extends string
? { renderOption?: never }
: { renderOption: (option: T) => JSX.Element };

type SelectProps<T> = {
name: string;
value: string;
label: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
options: Array<T>;
} & RenderOption<T>;

function Select<Option>(props: SelectProps<Option>) {
const { name, value, label, onChange, options, renderOption } = props;

return (
<label>
{label} &nbsp;
<select name={name} value={value} onChange={onChange}>
{options.map((opt, index) =>
!renderOption
? <option key={opt as string}>{opt as string}</option>
: <Fragment key={index}>{renderOption(opt)}</Fragment>
)}
</select>
</label>
);
}

export default Select;

Hints

  1. Select is a functional component with <Option> as generic type.
  2. The utility type RenderOption excludes the renderOption props using the never type if the option is of type string and makes it mandatory otherwise.
    This, by using conditional type.
  3. Type assertion is done because the TypeScript compiler cannot perform type inference inside the map callback.

Takeaway

Generics are one of the core features of TypeScript. Generics come along with a bunch of concepts, such as conditional type, utility type, distributivity of conditional type, type constraint, infer keyword.

The idea of Generics, is to pass the type of the function’s parameters as a parameter itself.

Learn more

If you’d like to learn more about TypeScript in general, check out another article I wrote.

In that article, we go deeper into Generics and also get into type narrowing, lookup type, satisfies operator, mapped type, infer keyword, const assertion, exhaustiveness checking, enum, algebraic data types, etc.

--

--