Generic type React component in TypeScript
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}
<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
Select
is a functional component with<Option>
as generic type.- The utility type
RenderOption
excludes therenderOption
props using thenever
type if the option is of typestring
and makes it mandatory otherwise.
This, by using conditional type. - 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.