36
loading...
This website collects cookies to deliver better user experience
value
and title
to render those, and an onChange
handler so that she can do something when a value in a select is changed (every select will do different things!).import React from 'react';
type SelectOption = {
value: string;
label: string;
};
type SelectProps = {
options: SelectOption[];
onChange: (value: string) => void;
};
export const Select = ({ options, onChange }: SelectProps) => {
return (
<select onChange={(e) => onChange(e.target.value)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
<>
<Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
<Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>
the select component accepts options in a very specific format, everything needs to be converted to it by the consumer component. And as the shop grows, more and more pages begin to use it, so that conversion code started to bleed all over the place and became hard to maintain.
onChange
handler returns only the id
of the changed value, so she needed to manually filter through arrays of data every time she needed to find the actual value that has changed
it's completely not typesafe, and very easy to make a mistake. Once she used doSomethingWithBooks
handler on a select with moviesOptions
by mistake, and that blew up the entire page and caused an incident. Customers were not happy 😞
onChange
handler returns the “raw” typed value, not just its id, hence removing the need to manually search for it on the consumer sideoptions
and onChange
values should be connected; so that if she uses doSomethingWithBooks
on a select that accepted movies as value, it would’ve been caught by the type system.export type Book = {
id: string;
title: string;
author: string; // only books have it
};
export type Movie = {
id: string;
title: string;
releaseDate: string; // only movies have it
};
... // all other types for the shop goods
type BookSelectProps = {
values: Book[];
onChange: (value: Book) => void;
};
export const BookSelect = ({ values, onChange }: BookSelectProps) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
BookSelect
into GenericSelect
and teach it how to deal with the rest of the data in the app. First, she just tried to do a union type on the values (if you’re not familiar with those - it’s just a fancy word for or
operator for types)onChange
callback with this approach, regardless of what goes into the values
. So even the most obvious and simple use case of logging the author of the selected book will make typescript super confused:Book
or Movie
, but it doesn’t know what exactly is there. And since Movie
doesn’t have an author field, typescript will consider the code above an error.function identity<Type>(a: Type): Type {
return a;
}
const a = identity<string>("I'm a string") // "a" will be a "string" type
const b = identity<boolean>(false) // "b" will be a "boolean" type
const a = identity<string>(false) // typescript will error here, "a" can't be boolean
const b = identity<boolean>("I'm a string") // typescript will error here, "b" can't be string
<Tvalue>
is a jsx
element and will fail. The second reason is exclusively generics problem: when we try to access value.title
or value.id
in our select, typescript at this point still doesn’t know which type we have in mind for this value. It has no idea which properties our value can have and rightfully so. Why would it?TValue
. Basically, it’s a way to tell typescript: I have no idea what TValue
should be yet, but I know for a fact that it will always have at least id
and title
, so you’re free to assume they will be there.type Base = {
id: string;
title: string;
};
type GenericSelectProps<TValue> = {
values: TValue[];
onChange: (value: TValue) => void;
};
export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
// This select is a "Book" type, so the value will be "Book" and only "Book"
<GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />
// This select is a "Movie" type, so the value will be "Movie" and only "Movie"
<GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />
useState
or useReducer
and avoid unfortunate copy-paste driven development mistakes, where you define const [book, setBook] = useState();
and then pass a movie
value there by accident. Things like that could cause a little crash of reality for the next person who reads the code and sees setBook(movie)
during the next refactoring.export const AmazonCloneWithState = () => {
const [book, setBook] = useState();
const [movie, setMovie] = useState();
return (
<>
<GenericSelect<Book> onChange={(value) => setMovie(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setBook(value)} values={moviesValues} />
</>
);
};
export const AmazonCloneWithState = () => {
const [book, setBook] = useState<Book | undefined>(undefined);
const [movie, setMovie] = useState<Movie | undefined>(undefined);
return (
<>
<GenericSelect<Book> onChange={(value) => setBook(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setMovie(value)} values={moviesValues} />
</>
);
};