24
loading...
This website collects cookies to deliver better user experience
switch
case that for every tab returns a select component, and a select component for categories themselves.const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
return (
<GenericSelect<Book> ... />
);
case "Movies":
return (
<GenericSelect<Movie> ... />
);
case "Laptops":
return (
<GenericSelect<Laptop> ... />
);
}
};
export const TabsComponent = () => {
const [tab, setTab] = useState<Tab>(tabs[0]);
const select = getSelect(tab);
return (
<>
Select category:
<GenericSelect<Tab>
onChange={(value) => setTab(value)}
values={tabs}
formatLabel={formatLabel}
/>
{select}
</>
);
};
Phones
? Seems easy enough: I just add it to the array and to the switch statement.const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
// ...
case "Phones":
return (
<GenericSelect<Phone> ... />
);
}
};
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// ...
case "Movies":
// ...
case "Laptops":
// ...
}
};
if
or switch
typescript performs what is known as “narrowing”, i.e. it reduces the available options for the union types with every statement. If, for example, we have a switch case with only “Books”, the “Books” type will be eliminated at the first case
statement, but the rest of them will be available later on:const tabs = ["Books", "Movies", "Laptops"] as const;
// Just "Books" in the switch statement
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// tab's type is Books here, it will not be available in the next cases
return <GenericSelect<Book> ... />
default:
// at this point tab can be only "Movies" or "Laptops"
// Books have been eliminated at the previous step
}
};
never
type.const tabs = ["Books", "Movies", "Laptops"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// "Books" have been eliminated here
case "Movies":
// "Movies" have been eliminated here
case "Laptops":
// "Laptops" have been eliminated here
default:
// all the values have been eliminated in the previous steps
// this state can never happen
// tab will be `never` type here
}
};
never
type. And if for some reason it’s not actually impossible (i.e. we added “Phones” to the array, but not the switch
- typescript will fail!// Added "Phones" here, but not in the switch
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
// Telling typescript explicitly that we want tab to be "never" type
// When this function is called, it should be "never" and only "never"
const confirmImpossibleState = (tab: never) => {
throw new Error(`Reacing an impossible state because of ${tab}`);
};
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// "Books" have been eliminated
case "Movies":
// "Movies" have been eliminated
case "Laptops":
// "Laptops" have been eliminated
default:
// This should be "impossible" state,
// but we forgot to add "Phones" as one of the cases
// and "tab" can still be the type "Phones" at this stage.
// Fortunately, in this function we assuming tab is always "never" type
// But since we forgot to eliminate Phones, typescript now will fail!
confirmImpossibleState(tab);
}
};
never
type and the “impossible” state. All you need is just to understand this process of narrowing and elimination, and how to “lock” the desired type at the last step.formatLabel
function that we pass to the select component, that returns the desired string for the select options based on the value type?export type DataTypes = Book | Movie | Laptop | string;
export const formatLabel = (value: DataTypes) => {
if (isBook(value)) return `${value.title}: ${value.author}`;
if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;
if (isLaptop(value)) return value.model;
return value;
};
Phone
as one of the data types, but forget the actual check? With the current implementation - nothing good again, the Phone select options will be broken. But, if we apply the exhaustiveness knowledge to the function, we can do this:export type DataTypes = Book | Movie | Laptop | Phone | string;
// When this function is called the value should be only string
const valueShouldBeString = (value: string) => value;
const formatLabel = (value: DataTypes) => {
// we're eliminating Book type from the union here
if (isBook(value)) return `${value.title}: ${value.author}`;
// here value can only be Movie, Laptop, Phone or string
// we're eliminating Movie type from the union here
if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;
// here value can only be Laptop, Phone or string
// we're eliminating Laptop type from the union here
if (isLaptop(value)) return value.model;
// here value can only be Phone or string
// But we actually want it to be only string
// And make typescript fail if it is not
// So we just call this function, that explicitly assigns "string" to value
return valueShouldBeString(value);
// Now, if at this step not all possibilities are eliminated
// and value can be something else other than string (like Phone in our case)
// typescript will pick it up and fail!
};
string
, and “locked” string in the final step. Pretty neat, huh?const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
enum Tabs {
'MOVIES' = 'Movies',
'BOOKS' = 'Books',
'LAPTOPS' = 'Laptops',
}
const movieTab = Tabs.MOVIES; // movieTab will be `Movies`
const bookTab = Tabs.BOOKS; // bookTab will be `Books`
Tabs
when you want to reference the enum as a type!